Провести очистку данных. Определить рыночные сегменты Меркур Авто и разработать маркетинговый план.
Для решения поставленной задачи мне предоставили датасет, в котором есть следующие столбцы:
# устанавливаю библиотеку транслитератор
!pip install transliterate
!pip install phik
# подключение необходимых юиюлиотек для исследования
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import re
import statistics as stat
import itertools
import seaborn as sns
import warnings
import phik
import plotly.graph_objects as go
import plotly.express as px
import plotly.io as pio
from plotly.subplots import make_subplots
from IPython.display import display_html
from phik.report import plot_correlation_matrix
from transliterate import translit
Requirement already satisfied: transliterate in c:\users\user\anaconda3\lib\site-packages (1.10.2) Requirement already satisfied: six>=1.1.0 in c:\users\user\anaconda3\lib\site-packages (from transliterate) (1.16.0) Requirement already satisfied: phik in c:\users\user\anaconda3\lib\site-packages (0.12.3) Requirement already satisfied: scipy>=1.5.2 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.9.1) Requirement already satisfied: numpy>=1.18.0 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.21.5) Requirement already satisfied: joblib>=0.14.1 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.1.0) Requirement already satisfied: pandas>=0.25.1 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.4.4) Requirement already satisfied: matplotlib>=2.2.3 in c:\users\user\anaconda3\lib\site-packages (from phik) (3.5.2) Requirement already satisfied: fonttools>=4.22.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (4.25.0) Requirement already satisfied: pillow>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (9.2.0) Requirement already satisfied: cycler>=0.10 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (0.11.0) Requirement already satisfied: packaging>=20.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (21.3) Requirement already satisfied: pyparsing>=2.2.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (3.0.9) Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (1.4.2) Requirement already satisfied: python-dateutil>=2.7 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (2.8.2) Requirement already satisfied: pytz>=2020.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.25.1->phik) (2022.1) Requirement already satisfied: six>=1.5 in c:\users\user\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib>=2.2.3->phik) (1.16.0)
# сохраняю датасет в переменную data
try:
data = pd.read_csv('C:/Users/USER/Desktop/Исходники/Анализ автомобильного рынка Казахстана/auto_kz_2019_датасет.csv',\
sep=';')
except:
print('FileNotFoundError')
# cброс ограничений на число столбцов
pd.set_option('display.max_columns', None)
# настраиваю количество знаков десятичных разрядов
pd.set_option('display.float_format', '{:.2f}'.format)
# устанавливаю стили визуализации
sns.set_style('darkgrid')
plt.style.use('ggplot')
pio.templates.default = 'seaborn'
# отключаю предупреждения
warnings.filterwarnings("ignore")
# функция для изучения информации по датасету
def dataset(set):
print('10 первых строк датасета')
display(set.head(10))
print('_____________________________________________________________')
print('Общая информация по датасету')
display(set.info())
print('_____________________________________________________________')
print('Числовое описание данных')
display(set.describe())
print('_____________________________________________________________')
print('Количество пропусков')
display(set.isna().sum())
print('_____________________________________________________________')
print('Kоличество пропусков для каждого столбца в процентах')
display(pd.DataFrame(round(set.isna().mean()*100, 2)).style.background_gradient('coolwarm'))
print('_____________________________________________________________')
print('Число явных дубликатов:')
display(set.duplicated().sum())
print('_____________________________________________________________')
print('Количество строк и столбцов в датасете')
display(set.shape)
print('_____________________________________________________________')
all_lines = len(set) # расчет количества строк для дальнейших расчетов
dataset(data)
10 первых строк датасета
| Год | Месяц | Компания | Бренд | Модель | Модификация | Год выпуска | Страна-производитель | Вид топлива | Объём двиг, л, | Коробка передач | Тип привода | Сегмент | Регион | Наименование дилерского центра | Тип клиента | Форма расчета | Количество | Цена, USD | Продажа, USD | Область | Сегментация 2013 | Класс 2013 | Сегментация Eng | Локализация производства | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2019 | Май | Mercur Auto | Audi | A3 | TFSI | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Физ. Лицо | безналичный | 1.00 | 28115 | 28115 | г.Алматы | Легковые автомобили | C класс | C | Импорт |
| 1 | 2019 | Август | Mercur Auto | Audi | A3 | TFSI | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Юр. Лицо | наличный | 1.00 | 32246,99 | 32246,99 | г.Алматы | Легковые автомобили | C класс | C | Импорт |
| 2 | 2019 | Апрель | Mercur Auto | Audi | A4 | TFSI | 2018 | Германия | Бензин | 1,4 | S-Tronic | FWD | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Физ. Лицо | безналичный | 1.00 | 32000 | 32000 | г.Алматы | Легковые автомобили | D класс | D | Импорт |
| 3 | 2019 | Июль | Mercur Auto | Audi | A4 | TFSI | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Юр. Лицо | безналичный | 1.00 | 31929 | 31929 | г.Алматы | Легковые автомобили | D класс | D | Импорт |
| 4 | 2019 | Июль | Mercur Auto | Audi | A4 | TFSI | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Физ. Лицо | наличный | 1.00 | 31929 | 31929 | г.Алматы | Легковые автомобили | D класс | D | Импорт |
| 5 | 2019 | Февраль | Mercur Auto | Audi | A6 | TFSI | 2017 | Германия | Бензин | 2 | S-Tronic | quattro | Легковые автомобили Premium | Нур-Султан | Mercur Auto Астана | Физ. Лицо | Наличная | 1.00 | 42608,25485 | 42608,25485 | г.Нур-Султан | Легковые автомобили | E класс | E | Импорт |
| 6 | 2019 | Май | Mercur Auto | Audi | A6 | TFSI | 2019 | Германия | Бензин | 3 | S-tronic | quattro | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Физ. Лицо | наличный | 1.00 | 82344 | 82344 | г.Алматы | Легковые автомобили | E класс | E | Импорт |
| 7 | 2019 | Июнь | Mercur Auto | Audi | A6 | TFSI | 2019 | Германия | Бензин | 3 | S-tronic | quattro | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Физ. Лицо | наличный | 1.00 | 78940 | 78940 | г.Алматы | Легковые автомобили | E класс | E | Импорт |
| 8 | 2019 | Июнь | Mercur Auto | Audi | A6 | TFSI | 2019 | Германия | Бензин | 3 | S-tronic | quattro | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Физ. Лицо | наличный | 1.00 | 78940 | 78940 | г.Алматы | Легковые автомобили | E класс | E | Импорт |
| 9 | 2019 | Июнь | Mercur Auto | Audi | A6 | TFSI Allroad | 2017 | Германия | Бензин | 3 | S-tronic | quattro | Легковые автомобили Premium | Алматы | Mercur Auto Алматы | Юр. Лицо | безналичный | 1.00 | 64474 | 64474 | г.Алматы | Легковые автомобили | E класс | E | Импорт |
_____________________________________________________________ Общая информация по датасету <class 'pandas.core.frame.DataFrame'> RangeIndex: 39966 entries, 0 to 39965 Data columns (total 25 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Год 39966 non-null int64 1 Месяц 39966 non-null object 2 Компания 39966 non-null object 3 Бренд 39966 non-null object 4 Модель 39966 non-null object 5 Модификация 36375 non-null object 6 Год выпуска 39465 non-null object 7 Страна-производитель 39966 non-null object 8 Вид топлива 36826 non-null object 9 Объём двиг, л, 35708 non-null object 10 Коробка передач 36711 non-null object 11 Тип привода 35677 non-null object 12 Сегмент 33205 non-null object 13 Регион 39966 non-null object 14 Наименование дилерского центра 39966 non-null object 15 Тип клиента 32919 non-null object 16 Форма расчета 14038 non-null object 17 Количество 39960 non-null float64 18 Цена, USD 39966 non-null object 19 Продажа, USD 39966 non-null object 20 Область 39966 non-null object 21 Сегментация 2013 39966 non-null object 22 Класс 2013 39966 non-null object 23 Сегментация Eng 39966 non-null object 24 Локализация производства 39966 non-null object dtypes: float64(1), int64(1), object(23) memory usage: 7.6+ MB
None
_____________________________________________________________ Числовое описание данных
| Год | Количество | |
|---|---|---|
| count | 39966.00 | 39960.00 |
| mean | 2019.00 | 1.35 |
| std | 0.00 | 2.37 |
| min | 2019.00 | -1.00 |
| 25% | 2019.00 | 1.00 |
| 50% | 2019.00 | 1.00 |
| 75% | 2019.00 | 1.00 |
| max | 2019.00 | 115.00 |
_____________________________________________________________ Количество пропусков
Год 0 Месяц 0 Компания 0 Бренд 0 Модель 0 Модификация 3591 Год выпуска 501 Страна-производитель 0 Вид топлива 3140 Объём двиг, л, 4258 Коробка передач 3255 Тип привода 4289 Сегмент 6761 Регион 0 Наименование дилерского центра 0 Тип клиента 7047 Форма расчета 25928 Количество 6 Цена, USD 0 Продажа, USD 0 Область 0 Сегментация 2013 0 Класс 2013 0 Сегментация Eng 0 Локализация производства 0 dtype: int64
_____________________________________________________________ Kоличество пропусков для каждого столбца в процентах
| 0 | |
|---|---|
| Год | 0.000000 |
| Месяц | 0.000000 |
| Компания | 0.000000 |
| Бренд | 0.000000 |
| Модель | 0.000000 |
| Модификация | 8.990000 |
| Год выпуска | 1.250000 |
| Страна-производитель | 0.000000 |
| Вид топлива | 7.860000 |
| Объём двиг, л, | 10.650000 |
| Коробка передач | 8.140000 |
| Тип привода | 10.730000 |
| Сегмент | 16.920000 |
| Регион | 0.000000 |
| Наименование дилерского центра | 0.000000 |
| Тип клиента | 17.630000 |
| Форма расчета | 64.880000 |
| Количество | 0.020000 |
| Цена, USD | 0.000000 |
| Продажа, USD | 0.000000 |
| Область | 0.000000 |
| Сегментация 2013 | 0.000000 |
| Класс 2013 | 0.000000 |
| Сегментация Eng | 0.000000 |
| Локализация производства | 0.000000 |
_____________________________________________________________ Число явных дубликатов:
18698
_____________________________________________________________ Количество строк и столбцов в датасете
(39966, 25)
_____________________________________________________________
Выводы:
Для дальнейших расчетов понадобится число строк датасета до предобработки, поэтому это значение сохраню в переменную all_lines.
all_lines = len(data)
Удаляю столбцы:
data = data.drop(['Сегментация Eng', 'Форма расчета', 'Сегмент', \
'Локализация производства', 'Наименование дилерского центра', 'Регион', 'Модификация'], axis=1)
data.head()
| Год | Месяц | Компания | Бренд | Модель | Год выпуска | Страна-производитель | Вид топлива | Объём двиг, л, | Коробка передач | Тип привода | Тип клиента | Количество | Цена, USD | Продажа, USD | Область | Сегментация 2013 | Класс 2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2019 | Май | Mercur Auto | Audi | A3 | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Физ. Лицо | 1.00 | 28115 | 28115 | г.Алматы | Легковые автомобили | C класс |
| 1 | 2019 | Август | Mercur Auto | Audi | A3 | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Юр. Лицо | 1.00 | 32246,99 | 32246,99 | г.Алматы | Легковые автомобили | C класс |
| 2 | 2019 | Апрель | Mercur Auto | Audi | A4 | 2018 | Германия | Бензин | 1,4 | S-Tronic | FWD | Физ. Лицо | 1.00 | 32000 | 32000 | г.Алматы | Легковые автомобили | D класс |
| 3 | 2019 | Июль | Mercur Auto | Audi | A4 | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Юр. Лицо | 1.00 | 31929 | 31929 | г.Алматы | Легковые автомобили | D класс |
| 4 | 2019 | Июль | Mercur Auto | Audi | A4 | 2018 | Германия | Бензин | 1,4 | S-tronic | передний | Физ. Лицо | 1.00 | 31929 | 31929 | г.Алматы | Легковые автомобили | D класс |
Переименую названия столбцов на английский язык с использованием змеиного регистра.
data = data.rename(columns={
'Год': 'year',
'Месяц': 'month',
'Компания': 'company',
'Бренд': 'brand',
'Модель': 'model',
'Год выпуска': 'year_of_release',
'Страна-производитель': 'producing_country',
'Вид топлива': 'type_of_fuel',
'Объём двиг, л,': 'engine_volume_l',
'Коробка передач': 'transmission',
'Тип привода': 'type_of_drive',
'Тип клиента': 'client_type',
'Количество': 'quantity',
'Цена, USD': 'price_usd',
'Продажа, USD': 'sale_usd',
'Область': 'area',
'Сегментация 2013': 'segmentation',
'Класс 2013': 'class_2013'
})
data.duplicated().sum()
22412
После удаления столбцов количество явных дубликатов увеличилось. Теперь их 20848. Предполагаю, что эти дубликаты возникли из-за деперсонализации, если бы были указаны id полных повторов бы не было. Потом что разные люди могут покупать одинаковые автомобили.
# функция для удаления лишних пробелов в строках
def space(df, column):
df[column] = df[column].str.strip()
# функция для приведения значений к нижнему регистру
def low(df, column):
df[column] = df[column].str.lower()
# функция для проверки на неявные дубликаты
def dubl(df, column): # 1-ый атрибут - переменная, харанящая датасет, 2-ая - название столбца
print("Число уникальных значений:", df[column].nunique()) # вывожу на экран количество уникальных значений
print("Уникальные значения:\n", df[column].unique()) # вывожу на экран группировку
# функция от неявных дубликатов и других неподходящих значений
def implicit_dubl(df, column, dictionary):
df[column] = df[column].replace(dictionary, regex=True)
print("Количество уникальных значений после очистки:", df[column].nunique())
# функция для заполнения пропусков модой
def change_mode(df, group1, group2, change_column):
df[change_column] = df.groupby([group1, group2])[change_column]\
.transform(lambda x: x.fillna(x.mode()[0]) if len(x.mode()) != 0 else x)
print('Количество незаполненных строк:', len(df[df[change_column].isna()]))
# функция для заполнения пропусков в столбцах по списку
def change_isna(df, cut_by_column, column, lis, replace_by):
df.loc[data[cut_by_column].isin(lis), column] = replace_by
# функция для удаления всех пробелов в значении
def space_del_all(df, column):
df[column] = df[column].replace(r' ', '', regex=True)
# функция для замены значений с помощью регулярных выражений
def re_sub(pat, df, column, name):
for i in pat:
df[column] = df[column].transform(lambda x: re.sub(i, name, str(x)))
# вызываю функцию, чтобы выявить неявные дубликаты
dubl(data, 'year')
Число уникальных значений: 1 Уникальные значения: [2019]
Как и заявлено в ТЗ данные предоставлены только за 2019 год. Неявных дубликатов нет.
# вызываю функцию для проверки на неявные дубликаты
dubl(data, 'month')
Число уникальных значений: 9 Уникальные значения: ['Май' 'Август' 'Апрель' 'Июль' 'Февраль' 'Июнь' 'Сентябрь' 'Январь' 'Март']
В датасете присутствуют данные с января по сентябрь. Здесь также нет неявных дубликатов. Заменю значения месяца на порядковый номер.
# создаю словарь для замены
change_month = {'Январь': 1, 'Февраль': 2, 'Март': 3, 'Апрель': 4,\
'Май': 5, 'Июнь': 6, 'Июль': 7, 'Август': 8, 'Сентябрь':9}
# меняю значения
data.month = data.month.map(change_month)
# вызываю функцию для вывода уникальных значений
dubl(data, 'company')
Число уникальных значений: 40 Уникальные значения: ['Mercur Auto' 'Автоцентр-Бавария' 'БИПЭК АВТО' 'Вираж' 'Eurasia Motor Premium' 'Daewoo Bus Kazakhstan' 'Caspian Motors' 'Каспиан Моторс' 'СемАЗ' 'Astana Motors' 'Astana Motors ' 'Hyundai Com Trans Kazakhstan ' 'Nissan Manufacturing RUS' 'СВС-ТРАНС' 'Allur Auto' 'Almaty Motors Premium' 'TERRA MOTORS' 'ТК КАМАЗ' 'Toyota Motor Kazakhstan' 'Лифан Моторс Рус' 'MAN Truck & Bus Kazakhstan' 'Автомир ГК' 'Autokapital' 'Автокапитал' 'ММС Рус' 'MMC RUS' 'Mercur Autos' 'УзАвто-Казахстан' 'Равон Моторс Казахстан' 'Ravon Motors Kazakstan' 'Renault Россия' 'Scania Central Asia' 'Subaru Kazakhstan' 'Volkswagen Group Rus' 'Автодом Motors KST' 'Автомир-Центр' 'ТОО "Eurasia Motor Zhaik"' 'Scandinavian Motors' 'Хино Моторс Казахстан' 'Hino Motors ']
Выявил следующие дубликаты:
Помимо дублей нашел ошибки:
# удаляю лишние пробелы
space(data, 'company')
# словарь для замены неявных дубликатов
change_company = {'Автокапитал': 'Autokapital',
'Хино Моторс Казахстан': 'Hino Motors',
'Каспиан Моторс': 'Caspian Motors',
'ММС Рус': 'MMC RUS',
'Mercur Autos': 'Mercur Auto',
'Равон Моторс Казахстан': 'Ravon Motors Kazakstan',
'Автомир ГК': 'Автомир-Центр'}
# вызываю функцию для замены значений
implicit_dubl(data, 'company', change_company)
Количество уникальных значений после очистки: 32
После работы с неявными дубликатами осталось 32 уникальные компании, которые продают автомобили.
# вывожу уникальные значения
dubl(data, 'brand')
Число уникальных значений: 46 Уникальные значения: ['Audi' 'BMW' 'Chevrolet' 'Daewoo' 'Ford' 'Foton' 'GAZ' 'Hyundai' 'Hyundai Truck & Bus' 'Infiniti' 'Isuzu' 'Iveco' 'Jac' 'Jaguar' 'KAMAZ' 'Kia' 'Lada' 'Land Rover' 'Lexus' 'Lifan' 'MAN' 'Mazda' 'Mercedes-Benz' 'Mercedes-Benz Trucks' 'Mini' 'Mitsubishi' 'Nissan' 'Peugeot' 'Porsche' 'Ravon' 'Renault' 'Scania' 'Skoda' 'Subaru' 'Toyota' 'UAZ' 'Volkswagen' 'Volvo' 'Урал' 'Cadillac' 'Shacman' 'Hino' 'Dong Feng' 'ANKAI' 'Nefaz' 'Yutong']
Считаю, что 'Mercedes-Benz Trucks' стоит объединить с 'Mercedes-Benz', так они относятся к одному концерну. Также лучше объединить 'Hyundai' c 'Hyundai Truck & Bus', так как 'Hyundai Truck & Bus' - это дочерняя компания и официальный представитель за пределами Южной Кореи. Также надо изменить на одно значение "Dong Feng" на "Dongfeng" (в переводе с китайского "восточный ветер").
# заменяю названия компаний
change_brand = {'Mercedes-Benz Trucks': 'Mercedes-Benz',
'Hyundai Truck & Bus': 'Hyundai',
'Dong Feng': 'Dongfeng'}
# вызываю функцию для замены значений
implicit_dubl(data, 'brand', change_brand)
Количество уникальных значений после очистки: 44
В датасете присутствуют 44 различных бренда.
# приведу все значения к нижнему регистру
low(data, 'model')
# вувожу уникальные значения
dubl(data, 'model')
Число уникальных значений: 273 Уникальные значения: ['a3' 'a4' 'a6' 'a7' 'a8' 'q5' 'q7' '3 серия' '5 серия' '6 серия' '7 серия' 'x1' 'x3' 'x4' 'x5' 'x6' 'x7' 'z4' 'aveo' 'captiva' 'niva' 'tahoe' 'tracker' 'bs 106 a' 'fiesta' 'mondeo' 'bj3253dmpkb-ad' '2217' '2310' '2705' '2752' '3221' '3302' '3308' '3309' 'next' 'accent' 'creta' 'elantra' 'santa fe' 'sonata' 'tq-1/h-1' 'county' 'h350' 'hd 160' 'hd 170' 'hd 65' 'hd 78' 'universe' 'tucson' 'q50' 'qx50' 'qx60' 'qx80' 'd-max' 'nmr' 'npr' 'daily' 'daily a' 'daily v' 'stralis' 'tipper' 's3' 's5' 'f-pace' 'f-type' 'xe' 'xf' 'xj' '4308' '4311' '43118' '43253' '43502' '44108' '45142' '45143' '53215' '53504' '53605' '54115' '5490' '65111' '65115' '65116' '65117' '6520' '65206' '65225' '66052' '6606' 'cerato' 'optima' 'picanto' 'quoris' 'rio' 'sorento' 'soul' 'sportage' '4x4' 'granta' 'kalina' 'largus' 'vesta' 'xray' 'вис' 'discovery' 'discovery sport' 'range rover' 'range rover evoque' 'range rover sport' 'es' 'gx' 'ls' 'lx' 'nx' 'rx' 'rxh' 'murman' 'solano' 'x50' 'x60' 'tg' '3' '6' 'cx-5' 'a-class' 'c-class' 'cla-class' 'cls-class' 'e-class' 'g-class' 'gla-class' 'glc-class' 'gle-class' 'gls-class' 's-class' 'actros' 'sprinter' 'tourismo' 'v-class' 'vito' 'countryman' 'asx' 'outlander' 'pajero iv' 'pajero sport' 'almera' 'juke' 'murano' 'qashqai' 'terrano' 'x-trail' '301' 'partner panel van' '911 carrera s' 'cayenne' 'macan' 'panamera' 'nexia r3' 'r4' 'duster' 'kaptur' 'koleos' 'logan' 'master' 'sandero' 'g-series' 'p-series' 'r-series' 'octavia' 'rapid' 'superb' 'forester' 'legacy' 'outback' 'xv' 'camry' 'corolla' 'highlander' 'hilux dc' 'lc prado' 'lc200' 'rav4' '2206' '3151' '3303' '3741' '3909' '3962' 'cargo' 'patriot' 'patriot pickup' 'amarok' 'caddy kasten ' 'caddy maxi kombi' 'caravelle' 'jetta' 'multivan' 'polo' 'tiguan' 'touareg' 'transporter' 'transporter kasten' 'xc90' '4320' '4320-1951-40' '4320-1951-60' '5557' 'myway' 'cx-9' 'hd 35' 'range rover velar' 'bs 106 d' 'kodiaq' 'escalade' 'glc coupe' 'x200' 'e-pace' 'dokker' 'eclipse cross' 'x70' 'n120' 'n75' 't6' 'sx3258dr384' 'iev' 's-series' 'nqr' 'x2' 's90' 'xc60' 'c-hr' '300' 'xc40' 'n56' 'xt5' '500' 'stinger' 'hatch' 'traverse' 'nms85' 'bj1069vdjea-f1' 'hd 79' 'bwc6665ga5' 'i8' 'ux' '8 серия' 'cabrio' 'hf-d105' 'nps' 'q8' 'rs5' 'teramont' '5299' 'i-pace' 's7' '45141' '32551' 'bc 211 ma' 'ex8' 'bc 095' 'camaro' 'hff6850g' 'transporter kombi ' 'gle coupe' 'hff6124g03ev3' 'hff6127gz-4' 'setra' 'zk6108hgh' 'arkana' 'v90' '58815z']
У значений 'transporter kombi ' и 'caddy kasten ' есть лишние пробелы, удаляю.
# удаляю лишние пробелы
space(data, 'model')
В Казахстане представлено 273 разные модели.
# вывожу уникальные значения
dubl(data, 'year_of_release')
Число уникальных значений: 9 Уникальные значения: ['2018' '2017' '2019' nan '2016' '2014' '2013' '2011' '2\xa0018' '2\xa0019']
В столбце есть пропуски, а также скрытые пробелы ('\xa0') после конвертации из Excel в csv.
# удаление ошибок
change_year_of_release = {r'\xa0': '', 'nan': np.NaN}
implicit_dubl(data, 'year_of_release', change_year_of_release)
Количество уникальных значений после очистки: 7
Далее необходимо заполнить пропуски.
Заполню пропуски модой. Если останутся пропуски, значит в этой категории автомобилей данных о годе выпуска нет. В таком случае поищу информацию в сторонних источниках.
# заполняю пропуски модой
change_mode(data, 'brand', 'model', 'year_of_release')
Количество незаполненных строк: 6
Выведу на экран строки с оставшимися пропусками.
data.query('year_of_release.isna()')
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 9631 | 2019 | 7 | Hyundai Com Trans Kazakhstan | Hyundai | hd 160 | NaN | Республика Казахстан | NaN | NaN | NaN | NaN | Юр. Лицо | 1.00 | 34000 | 34000 | г.Алматы | Коммерческие автомобили | Среднетоннажные грузовики |
| 20223 | 2019 | 2 | Автомир-Центр | Mazda | 3 | NaN | Российская Федерация | NaN | NaN | NaN | NaN | NaN | 1.00 | 21547,25584 | 21547,25584 | Карагандинская область | Легковые автомобили | C класс |
| 20224 | 2019 | 2 | Автомир-Центр | Mazda | 3 | NaN | Российская Федерация | NaN | NaN | NaN | NaN | NaN | 1.00 | 21547,25584 | 21547,25584 | г.Нур-Султан | Легковые автомобили | C класс |
| 20225 | 2019 | 3 | Автомир-Центр | Mazda | 3 | NaN | Российская Федерация | NaN | NaN | NaN | NaN | NaN | 1.00 | 21596,81698 | 21596,81698 | Карагандинская область | Легковые автомобили | C класс |
| 20226 | 2019 | 3 | Автомир-Центр | Mazda | 3 | NaN | Российская Федерация | NaN | NaN | NaN | NaN | NaN | 1.00 | 21596,81698 | 21596,81698 | г.Нур-Султан | Легковые автомобили | C класс |
| 20227 | 2019 | 3 | Автомир-Центр | Mazda | 3 | NaN | Российская Федерация | NaN | NaN | NaN | NaN | NaN | 1.00 | 21596,81698 | 21596,81698 | г.Нур-Султан | Легковые автомобили | C класс |
Осталось 2 вида автомобилей.
# заполняю строку со Hyundai hd 160
data.loc[data.model == "hd 160", 'year_of_release'] = 0
# заменяю значение в столбце 'year_of_release' у автомобиля Mazda 3 на моду
data.loc[data.model == "3", 'year_of_release'] = data.query('brand == "Mazda"').year_of_release.mode()[0]
# перевожу значение столбца в тип 'int'
data.year_of_release = data.year_of_release.astype(int)
dubl(data, 'producing_country')
Число уникальных значений: 19 Уникальные значения: ['Германия' 'США' 'Австрия' 'Республика Казахстан' 'Российская Федерация' 'Корея' 'Япония' 'Таиланд' 'Китай' 'UK' 'Узбекистан' 'Венгрия' 'Турция' 'Испания' 'Нидерланды' 'Польша' 'Швеция' 'Белоруссия' 'Бельгия']
В этом столбце проблем нет. В Казахстане продают автомобили из 19-ти стран.
# вывожу уникальные значения столбца
dubl(data, 'type_of_fuel')
Число уникальных значений: 10 Уникальные значения: ['Бензин' 'Дизель' nan 'дизель ' 'бензин ' 'гибрид' '2' '1,6' '0' 'Электро' 'Электричество']
Выводы:
С этими ошибками надо разобраться.
Для начала выведу строки, в которых в типе двигателя указан его объем.
data.query('type_of_fuel == "2" or type_of_fuel == "1,6"').head(10)
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23723 | 2019 | 1 | Renault Россия | Renault | duster | 2019 | Российская Федерация | 2 | MT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Западно-Казахстанская область | Внедорожники | Субкомпактные SUV |
| 23724 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | AT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | г.Нур-Султан | Внедорожники | Субкомпактные SUV |
| 23725 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | AT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | г.Нур-Султан | Внедорожники | Субкомпактные SUV |
| 23726 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | AT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Кызылординская область | Внедорожники | Субкомпактные SUV |
| 23727 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | MT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Карагандинская область | Внедорожники | Субкомпактные SUV |
| 23728 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 1,6 | MT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Карагандинская область | Внедорожники | Субкомпактные SUV |
| 23729 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | AT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Карагандинская область | Внедорожники | Субкомпактные SUV |
| 23730 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | AT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Карагандинская область | Внедорожники | Субкомпактные SUV |
| 23731 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | MT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Карагандинская область | Внедорожники | Субкомпактные SUV |
| 23732 | 2019 | 1 | Renault Россия | Renault | duster | 2018 | Российская Федерация | 2 | MT | 4WD | 4WD | NaN | 1.00 | 12249,61125 | 12249,61125 | Карагандинская область | Внедорожники | Субкомпактные SUV |
Видно, что значения в столбцах 'type_of_fuel', 'engine_volume_l', 'transmission' приняли значения из столбца справа. Чтобы это изменить надо:
# создаю датасет отфильтрованный по значению топлива в столбце 'type_of_fuel'
to_change = data.query('type_of_fuel in ["2", "1,6"]')
# замена значений в столбцах 'type_of_fuel', 'engine_volume_l' и 'transmission'
to_change['transmission'] = to_change['engine_volume_l']
to_change['engine_volume_l'] = to_change['type_of_fuel']
to_change['type_of_fuel'] = np.NaN
# объединяю датасеты to_change и data
data = pd.concat([data, to_change])
# удаляю строки в которых в столбце 'type_of_fuel' есть литраж.
data = data.query('type_of_fuel not in ["2", "1,6"]').reset_index(drop=True)
Значения "0" заменю на "NaN" в следующем разделе.
Чтобы убрать неявные дубликаты надо привести все к одному регистру (переведу в нижний), удалить пробелы и заменить строки со значением "электро" на "электричество".
# удаляю лишние пробелы
space(data, 'type_of_fuel')
# привожу к нижнему регистру
low(data, 'type_of_fuel')
# заменяю значения
change_type_of_fuel = {'0': np.NaN, 'электро': 'электричество'}
implicit_dubl(data, 'type_of_fuel', change_type_of_fuel)
Количество уникальных значений после очистки: 4
data.type_of_fuel.unique()
array(['бензин', 'дизель', nan, 'гибрид', 'электричество'], dtype=object)
Столбец "type_of_fuel" содержит категриальные значения. Эти пропуски можно заполнить по моде также, как и в п. 4.4.6.2. Но чтобы это сделать надо сгруппировать данные по столбцам 'brand' и 'model'.
# заполняю пропуски модой
change_mode(data, 'brand', 'model', 'type_of_fuel')
Количество незаполненных строк: 899
Выведу на экран модели с оставшимися пропусками.
# проверю остались ли пропуски
data.query('type_of_fuel.isna()').groupby(['brand', 'model']).agg({'type_of_fuel': 'size'}).head()
| type_of_fuel | ||
|---|---|---|
| brand | model | |
| ANKAI | hf-d105 | 30 |
| Dongfeng | bwc6665ga5 | 10 |
| Foton | bj1069vdjea-f1 | 19 |
| bj3253dmpkb-ad | 9 | |
| Hyundai | ex8 | 6 |
Эти пропуски заполню вручную. Информацию найду в интернете. По типам двигателя модели разделяются так:
Автомобилей Renault dokker выпускали и с дизельным и с бензиновым двигателями, поэтому для них поставлю значение "unknown".
# заполняю пропуски с дизельными автомобилями
disel_car = ['hf-d105', 'bwc6665ga5', 'bj1069vdjea-f1', 'bj3253dmpkb-ad', 'ex8', 'daily a', 'daily v'\
, 'stralis', 'tipper', 'koleos', 'master', 'sx3258dr384', '32551', '4320', '4320-1951-40',\
'4320-1951-60', '5557', 'hd 160']
change_isna(data, 'model', 'type_of_fuel', disel_car, 'дизель')
# заполню пропуски с бензиновыми автомобилями
petrol_car = ['3', '6', 'cx-5', 'cx-9', 'arkana', 'kaptur', 'logan']
change_isna(data, 'model', 'type_of_fuel', petrol_car, 'бензин')
# заполняю строки с Renault dokker случайными значениями
type_f = ['бензин', 'дизель'] # создаю список типов двигателей для замены
data.loc[(data.type_of_fuel.isna()) & (data.model == "dokker"), 'type_of_fuel'] = "unknown"
# проверяю на наличие пропусков
data.type_of_fuel.isna().sum()
0
# еще раз выведу уникальные значения типов топлива в двигателях
dubl(data, 'type_of_fuel')
Число уникальных значений: 5 Уникальные значения: ['бензин' 'дизель' 'гибрид' 'unknown' 'электричество']
В Казахстане продают автомобили с 4 типами двигателей: бензиновые, дизельные, гибридные и электрические. Значение "unknown" для автомобилей, у которых не удалось достоверно точно определить тип двигателя.
# вывожу уникальные значения столбца
dubl(data, 'engine_volume_l')
Число уникальных значений: 111 Уникальные значения: ['1,4' '2' '3' '4,4' '6,6' '1,5' '1,596' '2,4' '1,7' '26,7' '25,7' '24,7' '23,7' '22,7' '21,7' '20,7' '19,7' '18,7' '17,7' '16,7' '15,7' '14,7' '13,7' '12,7' '11,7' '10,7' '9,7' '8,7' '7,7' '6,7' '5,7' '4,7' '3,7' '2,7' '6,2' '1,4 Turbo' '2,5' '1,6' nan '2,8' '4,3' '4,3,' '1,6 MPI' '2,0 MPI' '2,4 GDI' '2,4 MPI' '2,5 CRDI VGT' '2,5 CRDI WGT' '3,9' '7,5' '12,3' '1,6 T-GDI' '2,0 CRDI' '2.0' '3,5' '5,6' '5,2' '3,0 L' '1.6' '1.5' '5' '1,598' '1,248' '1,998' '2,359' '1,999' '3,342' '1,591' '3,47' '1,69' '1,774' '2.5' '2.7 ' '3.5' '2.7' '4.6' '4,6' '5.7' '1,8' '10,5' '4' '5,5' '12' '12,8' '11' '2,2' '1,2' '1,485' '13' '1,33' '#Н/Д' '4.0' '2.4G' '2,4G' '2.8' '2,693' '2,0' '3,8' '3.8' '1,2T' '3,6' '7,6' '4,9' '2.0h' '2,9' '400 Л.С.' '4,98 L,' '4,98' '88 KWH' '8,4 L,' '6,7L' '6,5']
В этом столбце есть следующие проблемы:
Начну с отработки аномальных значений.
Комментарий ревьюера:
kwh это тоже мощность
# привожу к нижнему регистру
low(data, 'engine_volume_l')
# проверю, что за техника у которой указаны лошадиные силы и килловаты в час
data.query('engine_volume_l in ["400 л.с.", "88 kwh"]').head()
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 39507 | 2019 | 2 | Almaty Motors Premium | Jaguar | i-pace | 2019 | UK | электричество | 400 л.с. | РЕДУКТОР | 4WD | Физ. Лицо | 1.00 | 117236,9729 | 117236,9729 | г.Алматы | Внедорожники | Компактные SUV |
| 39508 | 2019 | 4 | Almaty Motors Premium | Jaguar | i-pace | 2019 | UK | электричество | 400 л.с. | РЕДУКТОР | 4WD | Физ. Лицо | 1.00 | 105388,7924 | 105388,7924 | г.Алматы | Внедорожники | Компактные SUV |
| 39509 | 2019 | 6 | Almaty Motors Premium | Jaguar | i-pace | 2019 | UK | электричество | 400 л.с. | РЕДУКТОР | 4WD | Юр. Лицо | 1.00 | 120413,2793 | 120413,2793 | г.Алматы | Внедорожники | Компактные SUV |
| 39781 | 2019 | 7 | Allur Auto | ANKAI | hff6124g03ev3 | 2019 | Республика Казахстан | электричество | 88 kwh | NaN | 2WD | Юр. Лицо | 1.00 | 307986,3633 | 307986,3633 | Костанайская область | Коммерческие автомобили | Большие автобусы |
| 39782 | 2019 | 7 | Allur Auto | ANKAI | hff6124g03ev3 | 2019 | Республика Казахстан | электричество | 88 kwh | NaN | 2WD | Юр. Лицо | 1.00 | 307986,3633 | 307986,3633 | Костанайская область | Коммерческие автомобили | Большие автобусы |
Из сторонних источников выяснил:
# заменю все запятые на точки и заменяю значения 400 л.с. и 88 kwh
change_engine_volume_l = {',': '.', '400 л.с.': '0', '88 kwh': '0', r'[^\d.]': ''}
implicit_dubl(data, 'engine_volume_l', change_engine_volume_l)
# заменяю '' на np.NaN
data.engine_volume_l = data.engine_volume_l.replace('', np.NaN, regex=True)
# удаляю лишние точки вконце значений
data.engine_volume_l = data.engine_volume_l.str.strip('.')
dubl(data, 'engine_volume_l')
Количество уникальных значений после очистки: 85 Число уникальных значений: 82 Уникальные значения: ['1.4' '2' '3' '4.4' '6.6' '1.5' '1.596' '2.4' '1.7' '26.7' '25.7' '24.7' '23.7' '22.7' '21.7' '20.7' '19.7' '18.7' '17.7' '16.7' '15.7' '14.7' '13.7' '12.7' '11.7' '10.7' '9.7' '8.7' '7.7' '6.7' '5.7' '4.7' '3.7' '2.7' '6.2' '2.5' '1.6' nan '2.8' '4.3' '2.0' '3.9' '7.5' '12.3' '3.5' '5.6' '5.2' '3.0' '5' '1.598' '1.248' '1.998' '2.359' '1.999' '3.342' '1.591' '3.47' '1.69' '1.774' '4.6' '1.8' '10.5' '4' '5.5' '12' '12.8' '11' '2.2' '1.2' '1.485' '13' '1.33' '4.0' '2.693' '3.8' '3.6' '7.6' '4.9' '2.9' '0' '4.98' '8.4' '6.5']
После удаления буквенных символов переведу столбец в тип float.
# переведу значения столбца в тип float
data.engine_volume_l = data.engine_volume_l.astype(float)
# выведу на экран технику, у которой объем двигателя более 10 литров
data.query('engine_volume_l > 10').groupby(['brand', 'model', 'engine_volume_l']).size().head()
brand model engine_volume_l
Chevrolet niva 10.70 1
11.70 1
12.70 1
13.70 1
14.70 1
dtype: int64
Исходя из сторонних источников делаю следующие выводы:
Кроме Chevrolet Niva выбивающихся значений не увидел, рассмотрю этот автомобиль подробнее.
# отфильтрую датасет по модели автомобиля Niva и сгруппирую по объему двигателя
data.query('model in ["niva"]').groupby(by='engine_volume_l').size().head()
engine_volume_l 1.70 542 2.70 1 3.70 1 4.70 1 5.70 1 dtype: int64
Вижу, что большинство автомобилей Нива с двигателем 1,7 л, остальные представлены с разными объемами в одном экземпляре. Это точно ошибка. Все ошибочные значения заменю на объем 1,7.
# заменяю значения для автомобиля 'niva'
data.loc[data.model == 'niva', 'engine_volume_l'] = 1.7
# снова вывожу посмотреть уникальные значения
dubl(data, 'engine_volume_l')
Число уникальных значений: 58 Уникальные значения: [ 1.4 2. 3. 4.4 6.6 1.5 1.596 2.4 1.7 6.2 2.5 1.6 nan 2.7 2.8 4.3 3.9 7.5 12.3 3.5 5.6 8.7 5.2 5. 1.598 1.248 1.998 2.359 1.999 3.342 1.591 3.47 1.69 1.774 4.6 5.7 1.8 10.5 4. 5.5 12. 12.8 11. 2.2 1.2 1.485 13. 1.33 2.693 3.8 3.6 7.6 4.9 2.9 0. 4.98 8.4 6.7 6.5 ]
Теперь проверю строки с объемом двигателя равным 0.
data.query('engine_volume_l == 0').groupby(by=['brand', 'model', 'type_of_fuel']).size().shape[0]
2
Это значит, что всего две модели имеют значение 0. Для них значение "0" устанавливал сам, других таких нет. Поэтому проверять это значение не надо. Далее буду заполнять пропуски.
Для начала заменю пропуски у автомобилей с электрическим двигателем. Такие пропуски можно будет заполнить значением 0, поскольку там нет цилиндров, по которым можно посчитать рабочий объем.
data.loc[data.type_of_fuel == 'электричество', 'engine_volume_l'] = 0
Теперь буду работать с оставшимися пропусками.
# составляю таблицу с пропусками по моделям
engine_vol_isna = (data
.query('engine_volume_l.isna()')
.groupby(by=['brand', 'model'])['engine_volume_l']
.size()
.reset_index()
)
# таблица без пропусков по моделям, которые есть в таблице engine_vol_isna
engine_vol_notna = data.query('model in @engine_vol_isna.model and engine_volume_l.notna()')\
.groupby(by=['brand', 'model'])['engine_volume_l'].size().reset_index()
# объединю таблицы engine_vol_notna и engine_vol_isna, чтобы посмотреть можно ли какие-то значения заменить модой
engine_vol_isna = (engine_vol_isna
.merge(engine_vol_notna, on=['brand', 'model'], how='left', suffixes=('_isna_count', '_notna_count'))
)
engine_vol_isna.sort_values('engine_volume_l_notna_count', ascending=False).head()
| brand | model | engine_volume_l_isna_count | engine_volume_l_notna_count | |
|---|---|---|---|---|
| 79 | Ravon | nexia r3 | 361 | 1338.00 |
| 34 | Jac | s3 | 175 | 1111.00 |
| 90 | Toyota | corolla | 2 | 1087.00 |
| 9 | GAZ | 3302 | 127 | 871.00 |
| 91 | Toyota | hilux dc | 2 | 825.00 |
Помимо значений из таблицы, из сторонних источников выяснил, что у части автомобилей устанавливали только один тип двигателя. Исходя из полученных данных установил, что следующие значения можно заполнить модой:
# создам список с моделями авто, которые буду заполнять модой
list_car = ['nexia r3',
's3',
'corolla',
'3302',
'hilux dc',
'polo',
'next',
's5',
'hff6850g',
'2705',
'n120',
'r4',
'3221',
't6',
'3308',
'2752',
'n56',
'xc60',
'range rover velar',
'xc90',
'caravelle',
'npr',
'301']
# заполняю пропуски модой и сохраняю все изменения в исходную переменную data
data_copy = data.query('model in @list_car')
change_mode(data_copy, 'brand', 'model', 'engine_volume_l')
# убираю строки в исходном датасете, которые заменял в копии
data = data.query('model not in @data_copy.model')
# добавляю в исходный датасет измененные строки
data = pd.concat([data, data_copy]).reset_index(drop=True)
# проверю, не потерял ли строки и столбцы после соединения
data.shape
Количество незаполненных строк: 0
(39966, 18)
При замене пропусков данные не потерял. Далее выведу на экран технику, которую не удалось заполнить модой.
(data
.query('engine_volume_l.isna()')
.groupby(['brand', 'model'])['engine_volume_l']
.size()
.reset_index()
.sort_values(by='engine_volume_l', ascending=False)
.head()
)
| brand | model | engine_volume_l | |
|---|---|---|---|
| 66 | Renault | duster | 650 |
| 71 | Renault | sandero | 435 |
| 69 | Renault | logan | 320 |
| 43 | KAMAZ | 65115 | 259 |
| 67 | Renault | kaptur | 230 |
Заполню пропуски для следующих моделей вручную на основании данных, полученных в сторонних источниках:
Для остальных моделей поставлю значение unknown, потому что у этих типов автомобилей несколько видов двигателей с разным объемом.
Также определил, что у автобусов Daewoo неверно указан объем двигателя - 2.5, а должно быть 7.64.
# изменю значения для автобусов Daewoo
data.loc[data.brand == "Daewoo", 'engine_volume_l'] = 7.64
# функция для заполнения пропусков в столбцах по списку
def change_isna(df, cut_by_column, column, lis, replace_by):
df.loc[data[cut_by_column].isin(lis), column] = replace_by
# составляю список авто с обемом двигателя 1.6 и заполняю пропуски
list_1_6 = ['sandero', 'logan', 'partner panel van']
change_isna(data, 'model', 'engine_volume_l', list_1_6, 1.6)
# составляю список авто с обемом двигателя 2.0 и заполняю пропуски
list_2 = ['x200', 'e-pace', 'discovery sport', 'transporter kasten']
change_isna(data, 'model', 'engine_volume_l', list_2, 2.0)
# составляю список авто с обемом двигателя 2.3 и заполняю пропуски
list_2_3 = ['master']
change_isna(data, 'model', 'engine_volume_l', list_2_3, 2.3)
# составляю список авто с обемом двигателя 2.3 и заполняю пропуски
list_2_5 = ['h350', 'cx-9', 'hd 35']
change_isna(data, 'model', 'engine_volume_l', list_2_5, 2.5)
# составляю список авто с обемом двигателя 2.98 и заполняю пропуски
list_2_98 = ['bwc6665ga5']
change_isna(data, 'model', 'engine_volume_l', list_2_98, 2.98)
# составляю список авто с обемом двигателя 3.5 и заполняю пропуски
list_3_5 = ['nms85']
change_isna(data, 'model', 'engine_volume_l', list_3_5, 3.5)
# составляю список авто с обемом двигателя 3.8 и заполняю пропуски
list_3_8 = ['bj1069vdjea-f1', 'n75']
change_isna(data, 'model', 'engine_volume_l', list_3_8, 3.8)
# составляю список авто с обемом двигателя 3.9 и заполняю пропуски
list_3_9 = ['hd 78', 'hd 65']
change_isna(data, 'model', 'engine_volume_l', list_3_9, 3.9)
# составляю список авто с обемом двигателя 5.2 и заполняю пропуски
list_5_2 = ['nps', 'nqr']
change_isna(data, 'model', 'engine_volume_l', list_5_2, 5.2)
# составляю список авто с обемом двигателя 6.7 и заполняю пропуски
list_6_7 = ['45143', '65116', '65117', '43253', '43502', '53605', '4308']
change_isna(data, 'model', 'engine_volume_l', list_6_7, 6.7)
# составляю список авто с обемом двигателя 7.545 и заполняю пропуски
list_7_545 = ['hd 160']
change_isna(data, 'model', 'engine_volume_l', list_7_545, 7.545)
# заменяю у автобусов 7.6 и заменяю значения
list_7_6 = ['bs 106 d', 'bc 211 ma', 'bc 095', 'bs 106 a']
change_isna(data, 'model', 'engine_volume_l', list_7_6, 7.6)
# составляю список авто с обемом двигателя 10.7 и заполняю пропуски
list_10_7 = ['setra']
change_isna(data, 'model', 'engine_volume_l', list_10_7, 10.7)
# составляю список авто с обемом двигателя 10.85 и заполняю пропуски
list_10_85 = ['45142', '54115', '45141', '53215']
change_isna(data, 'model', 'engine_volume_l', list_10_85, 10.85)
# составляю список авто с обемом двигателя 11.15 и заполняю пропуски
list_11_15 = ['4320-1951-60', '4320-1951-40', '32551']
change_isna(data, 'model', 'engine_volume_l', list_11_15, 11.15)
# составляю список авто с обемом двигателя 11.76 и заполняю пропуски
list_11_76 = ['65111', '53504', '65225']
change_isna(data, 'model', 'engine_volume_l', list_11_76, 11.76)
# составляю список авто с обемом двигателя 12.7 и заполняю пропуски
list_12_7 = ['universe']
change_isna(data, 'model', 'engine_volume_l', list_12_7, 12.7)
print('Количество оставшихся пропусков:', len(data.query('engine_volume_l.isna()')))
Количество оставшихся пропусков: 1962
Оставшиеся пропуски оставлю незаполненными.
Комментарий ревьюера:
тут лучше НаН оставить, чтобы статистика столбца не изменить (он числовой)
# выведу уникальные значения столбца
dubl(data, 'transmission')
Число уникальных значений: 81 Уникальные значения: ['S-tronic' 'S-Tronic' 'Tiptronic' 'АКПП' 'АКП' 'Steptronic' 'AT' '6 АТ' '5 МТ' '6АТ' 'MT' '8AT' '5МТ' 'Powershift S6' '6AT' nan 'Мех.' '6 AT' '6 MT' '6MT' '5 AT' '5AT' 'МКПП' '7 DCT' '7DCT' '8 AT' 'МТ' '4 АТ' '6 МТ' '6МТ' '4АТ' '4AT' '8АТ' '8 АТ' '5MT' 'AMT' 'CVT (вариатор)' 'CVT (вариATор)' 'CVT(вAриATор)' 'CVT' '7G-TRONIC' '9G-TRONIC' '12AT' '12АТ' '5АТ' '7АТ' '7AT' '6M/T' 'PDK' '8-ступ АКПП' '8' '8АКПП' '8 АКПП Tiptronic S' '7 АКПП (PDK)' '7 АКПП PDK' '8 АКПП (PDK)' '7 DSG' '7DSG' '6 DSG' '6DSG' 'DSG' '6A' '6А' ' 7 АКП' 'АТ' 'A/T' '7АКП' 'А/T' '8A' '9AT' 'РЕДУКТОР' 'DCT' '0' 'АT' 'МT' '#Н/Д' '5M' 'M/T' 'МКП' '5М' 'М/T' 'TDI']
Теперь переведу все значения в нижний регистр, заменю кириллицу на латиницу и уберу все пробелы в значениях.
# переведу все значения в нижний регистр
low(data, 'transmission')
# заменяю кириллицу на латиницу в столбце
data.transmission = data['transmission'].transform(lambda x: translit(str(x), 'ru', reversed=True))
# удаляю пробелы в значениях
space_del_all(data, 'transmission')
Теперь сделаю функцию для замены значений с помощью регулярных выражений.
# изменяю на значение 'вариатор'
pattern_var = ['cvt\S+', 'cvt']
re_sub(pattern_var, data, 'transmission', 'вариатор')
# изменяю на значение 'робот'
pattern_robot = ['s-tronic', '\d+dsg', 'dsg', '\d+dct', 'dct', 'powershifts6', 'pdk']
re_sub(pattern_robot, data, 'transmission', 'робот')
# изменяю на значение 'автомат'
pattern_acpp = ['\d+akpp\S+','\S+akpp', 'akpp', '\S+akp', 'akp', '\S+at', 'at', 'a\St', '\da', '8', '\S+tronic']
re_sub(pattern_acpp, data, 'transmission', 'автомат')
# изменяю на значение 'механика'
pattern_mech = ['reduktor', 'mkpp', 'mkp', 'tdi', '\Smt', '5m', '\Sm/t', 'm/t', 'mt', 'meh.']
re_sub(pattern_mech, data, 'transmission', 'механика')
# заполняю оставшиеся значения np.NaN
change_transmission = {'0': np.NaN, 'nan': np.NaN, '#n/d': np.NaN}
implicit_dubl(data, 'transmission', change_transmission)
Количество уникальных значений после очистки: 4
Сведу в одну таблицу число пропусков и наличие значений по каждой модели.
# создаю таблицу с числом пропусков
transmission_isna = (data
.query('transmission.isna()')
.groupby(by=['brand', 'model'])['transmission']
.size()
.reset_index()
)
# таблица без пропусков по моделям, которые есть в таблице transmission_isna
transmission_notna = data.query('model in @transmission_isna.model and transmission.notna()')\
.groupby(by=['brand', 'model'])['transmission'].size().reset_index()
# свожу таблицы transmission_notna и transmission_isna в одну, чтобы посмотреть можно ли какие-то значения заменить модой
transmission_isna = (transmission_isna
.merge(transmission_notna, on=['brand', 'model'], how='left', suffixes=('_isna_count', '_notna_count'))
)
transmission_isna.sort_values(by='transmission_notna_count', ascending=False).head()
| brand | model | transmission_isna_count | transmission_notna_count | |
|---|---|---|---|---|
| 48 | Ravon | nexia r3 | 361 | 1338.00 |
| 27 | Jac | s3 | 175 | 1111.00 |
| 63 | Toyota | corolla | 2 | 1087.00 |
| 64 | Toyota | hilux dc | 2 | 825.00 |
| 67 | Volkswagen | polo | 28 | 516.00 |
Помимо значений из таблицы, из сторонних источников выяснил, что у части автомобилей устанавливали только один тип двигателя. Исходя из полученных данных сохраню в переменную unknown_tr модели, которые правильно не заполнить. Для них вместо пропусков поставлю значение unknown.
unknown_tr = ['s5',
'tiguan',
'duster',
'amarok',
'daily',
'sandero',
'hf-d105',
'hff6124g03ev3',
'd-max',
'daily a',
'3',
'6',
'cx-5',
'setra',
'arkana',
'kaptur',
'logan',
'g-series',
'p-series',
'r-series',
's-series']
# заполняю пропуски значением 'unknown' для моделей из списка unknown_tr
change_isna(data, 'model', 'transmission', unknown_tr, 'unknown')
Есть один автомобиль, на который устанавливали вариатор. Его внесу в список var_tr и заполню для него пропуск значением 'вариатор'.
var_tr = ['koleos']
change_isna(data, 'model', 'transmission', var_tr, 'вариатор')
Теперь заполню пропуски для машин с коробкой автомат.
auto_tr = ['cx-9', 'iev']
change_isna(data, 'model', 'transmission', auto_tr, 'автомат')
Далее создам список с моделями, пропуски которых заполню значением "механика".
mech_tr = ['hff6850g',
'n120',
'bwc6665ga5',
'bj1069vdjea-f1',
'bj3253dmpkb-ad',
'ex8',
'hd 160',
'nmr',
'nms85',
'npr',
'nps',
'nqr',
'daily v',
'tipper',
'tg',
'dokker',
'master',
'sx3258dr384',
'32551',
'4320',
'4320-1951-40',
'4320-1951-60',
'5557']
change_isna(data, 'model', 'transmission', mech_tr, 'механика')
# заполняю оставшиеся пропуски медианным значением
change_mode(data, 'brand', 'model', 'transmission')
Количество незаполненных строк: 0
# оставшиеся пропуски заполняю значением 'unknown'
data.transmission.unique()
array(['робот', 'автомат', 'механика', 'unknown', 'вариатор'],
dtype=object)
Пропусков в столбце нет - порядок.
# переведу все значения в нижний регистр
low(data, 'type_of_drive')
# удаляю пробелы в значениях
data.type_of_drive = data.type_of_drive.replace(r' ', '', regex=True)
# заменяю кириллицу на латиницу в столбце
data.type_of_drive = data['type_of_drive'].transform(lambda x: translit(str(x), 'ru', reversed=True))
# выведу уникальне значения
dubl(data, 'type_of_drive')
Число уникальных значений: 21 Уникальные значения: ['perednij' 'fwd' 'quattro' 'zadnij' 'polnyj' 'awd' '4wd' '4h2.2' 'nan' 'rwd' '2wd' '4x4' '4h4' '4x2' '4h2' 'astana' '0' 'ff' '4motion' 'perednij(ff)' '#n/d']
Есть неявные дубликаты, пропуски и аномалии.
Аномалии в столбце следующие:
Сначала посмотрю, на строки со значением 'astana'.
data.query('type_of_drive == "astana"')
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12259 | 2019 | 5 | ТК КАМАЗ | KAMAZ | 65116 | 2019 | Республика Казахстан | дизель | 6.70 | механика | astana | Юр. Лицо | 1.00 | 55655,86316 | 55655,86316 | г.Нур-Султан | Коммерческие автомобили | Крупнотоннажные грузовики |
| 31419 | 2019 | 5 | ТК КАМАЗ | KAMAZ | 45141 | 2019 | Республика Казахстан | дизель | 10.85 | механика | astana | Юр. Лицо | 1.00 | 53963,81966 | 53963,81966 | г.Нур-Султан | Коммерческие автомобили | Крупнотоннажные грузовики |
Эти значения в двух строках. Для KAMAZ 65116 в любой комплектации характерен задний (FR) тип привода. Для KAMAZ 45141 - полный привод (4WD). Заполню значения для этой техники.
data.loc[data.model == '65116', 'type_of_drive'] = 'FR'
data.loc[data.model == '45141', 'type_of_drive'] = '4WD'
# заменю '#n/d' и '0' на pd.NaN
data.loc[(data.type_of_drive == '#n/d') |\
(data.type_of_drive == '0') |\
(data.type_of_drive == 'nan'), 'type_of_drive'] = np.NaN
Значения '4x2', '4h2' неявные дубликаты. Они означают, что автомобиль может оснащаться либо передним, либо задним типом привода. Узнаю про эту технику подробнее, чтобы получить точную информацию. Также сразу проверю значение '4h2.2'.
data.query('type_of_drive in ["4x2", "4h2", "4h2.2"]').sort_values('type_of_drive').head()
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11462 | 2019 | 9 | СВС-ТРАНС | Isuzu | nmr | 2019 | Российская Федерация | дизель | NaN | механика | 4h2 | NaN | 1.00 | 29194,21386 | 29194,21386 | г.Алматы | Коммерческие автомобили | Малотоннажные грузовики |
| 11452 | 2019 | 9 | СВС-ТРАНС | Isuzu | nmr | 2019 | Российская Федерация | дизель | NaN | механика | 4h2 | NaN | 1.00 | 29194,21386 | 29194,21386 | г.Алматы | Коммерческие автомобили | Малотоннажные грузовики |
| 11453 | 2019 | 9 | СВС-ТРАНС | Isuzu | nmr | 2019 | Российская Федерация | дизель | NaN | механика | 4h2 | NaN | 1.00 | 29194,21386 | 29194,21386 | г.Алматы | Коммерческие автомобили | Малотоннажные грузовики |
| 11454 | 2019 | 9 | СВС-ТРАНС | Isuzu | nmr | 2019 | Российская Федерация | дизель | NaN | механика | 4h2 | NaN | 1.00 | 29194,21386 | 29194,21386 | г.Алматы | Коммерческие автомобили | Малотоннажные грузовики |
| 11455 | 2019 | 9 | СВС-ТРАНС | Isuzu | nmr | 2019 | Российская Федерация | дизель | NaN | механика | 4h2 | NaN | 1.00 | 29194,21386 | 29194,21386 | г.Алматы | Коммерческие автомобили | Малотоннажные грузовики |
Значения '4h2' принадлежат автомоблию Isuzu nmr, а '4x2' - это Isuzu nqr, Mercedes-Benz actros, Isuzu nmr, Isuzu npr. Значения '4h2.2' имеют: Daewoo bs 106 d, Daewoo bc 211 ma, Daewoo bc 095, Daewoo bs 106 a.
Из сторонних источников выяснил, что вся эта техника имеет задний привод (FR). Можно заменить. Также обнаружил, что у моделей 'pajero iv', 'cayenne', 'macan' ошибочно установлено значение '2wd'. Их надо поменять на '4WD'.
dubl(data, 'type_of_drive')
Число уникальных значений: 19 Уникальные значения: ['perednij' 'fwd' 'quattro' 'zadnij' 'polnyj' 'awd' '4wd' '4h2.2' nan 'rwd' '2wd' '4x4' '4h4' '4x2' '4h2' 'FR' 'ff' '4motion' '4WD' 'perednij(ff)']
# функция для замены неявных дубликатов
def type_drive(cell):
if cell in ['perednij', 'fwd', 'ff', 'perednij(ff)']:
return "FF"
elif cell in ['zadnij', '4h2', '4x2', '4h2.2', 'rwd']:
return "FR"
elif cell in ['quattro', 'polnyj', '4wd', '4motion', '4x4', '4h4']:
return "4WD"
elif cell in ['awd']:
return 'AWD'
else:
return cell
# вызываю функцию для замены неявных дубликатов
data.type_of_drive = data.type_of_drive.apply(type_drive)
Теперь надо заменить значения "2wd" на "FF" или "FR". Для этого создам список для переднеприводных автомобилей.
# список с переднеприводных автомобилей
list_2wd_ff = ['accent',
'creta',
'elantra',
'sonata',
'tucson',
's3',
's5',
's7',
'asx',
'eclipse cross',
'outlander',
'qashqai',
'x-trail',
'301',
'partner panel van',
'nexia r3',
'r4',
'corolla',
'caddy kasten',
'caddy maxi kombi',
'polo',
'tiguan',
's90'
]
# замена значений '2wd' на 'FF'
data.loc[(data['model'].isin(list_2wd_ff)) & (data['type_of_drive'] == "2wd"), 'type_of_drive'] = 'FF'
В датасете обнуражил, что у 'pajero iv', 'cayenne', 'macan' указан тип привода '2wd', а должен быть '4WD'. Это надо исправить.
# заменяю ошибочные '2wd' на '4WD'
data.loc[(data['model']\
.isin(['pajero iv', 'cayenne', 'macan'])) & (data['type_of_drive'] == "2wd"), 'type_of_drive'] = '4WD'
# заменяю оставшиеся '2wd' на 'FR'
data.loc[(data['type_of_drive'] == "2wd"), 'type_of_drive'] = 'FR'
Осталось заполнить пропуски.
# составляю таблицу с пропусками по моделям
type_dr_isna = (data
.query('type_of_drive.isna()')
.groupby(by=['brand', 'model'])['type_of_drive']
.size()
.reset_index()
)
# таблица без пропусков по моделям, которые есть в таблице transmission_isna
type_dr_notna = data.query('model in @type_dr_isna.model and type_of_drive.notna()')\
.groupby(by=['brand', 'model'])['type_of_drive'].size().reset_index()
# свожу таблицы transmission_notna и transmission_isna в одну, чтобы посмотреть можно ли какие-то значения заменить модой
type_dr_isna = (type_dr_isna
.merge(type_dr_notna, on=['brand', 'model'], how='left', suffixes=('_isna_count', '_notna_count'))
)
type_dr_isna.sort_values(by='type_of_drive_notna_count', ascending=False).head()
| brand | model | type_of_drive_isna_count | type_of_drive_notna_count | |
|---|---|---|---|---|
| 66 | Ravon | nexia r3 | 361 | 1338.00 |
| 81 | Toyota | corolla | 2 | 1087.00 |
| 22 | Jac | s3 | 317 | 969.00 |
| 82 | Toyota | hilux dc | 2 | 825.00 |
| 23 | Jac | s5 | 105 | 342.00 |
Из имеющихся данных можно подобрать значения с использованием моды кроме: Jaguar xj, Land Rover discovery sport, Mercedes-Benz v-class, Volkswagen amarok, Renault duster. Эти автомобили не подходят, потому что у них есть комплектации с разными типами привода. Для них поставлю значение 'unknown'. Это же значение присвою для: Shacman sx3258dr384, Renault master, Renault koleos, Renault kaptur, Renault arkana, Mazda cx-9, Mazda cx-5, Mazda 6, Mazda 3, MAN tg.
Данные заполняемые вручную:
Полный привод: KAMAZ 4311, 43118, 43502, 44108, 53504, 65111, 65225, Scania r-series, все Уралы.
Передний привод: Jac iev, Renault logan, Renault dokker.
Задний привод - все остальные пропуски.
# заполняю пропуски значением 'unknown'
unk_drive = ['sx3258dr384', 'master', 'koleos', 'kaptur', 'arkana', 'cx-9', 'cx-5', '6', '3', 'tg']
change_isna(data, 'model', 'type_of_drive', unk_drive, 'unknown')
# заполняю пропуски значением 'FF'
ff_drive = ['iev', 'logan', 'dokker']
change_isna(data, 'model', 'type_of_drive', ff_drive, 'FF')
# заполняю пропуски значением '4WD'
drive_4wd = ['4311', '43118', '43502', '44108', '53504', '65111',\
'65225', 'r-series', '32551', '4320', '4320-1951-40', '4320-1951-60', '5557']
change_isna(data, 'model', 'type_of_drive', drive_4wd, '4WD')
# заполняю пропуски модой
change_mode(data, 'brand', 'model', 'type_of_drive')
Количество незаполненных строк: 663
# оставшиеся пропуски заполняю значением 'FR'
data.type_of_drive.fillna('FR', inplace=True)
Пропуски заполнил.
# заменяю ',' на '.'
data[['price_usd', 'sale_usd']] = data[['price_usd', 'sale_usd']].replace(',', '.', regex=True)
# изменяю тип данных на float
data[['price_usd', 'sale_usd']] = data[['price_usd', 'sale_usd']].astype(float)
# вывожу численную характеристику столбцов
data[['price_usd', 'sale_usd']].describe()
| price_usd | sale_usd | |
|---|---|---|
| count | 39966.00 | 39966.00 |
| mean | 28250.16 | 33757.67 |
| std | 20005.76 | 50702.45 |
| min | 7100.00 | -35588.25 |
| 25% | 15518.50 | 16601.93 |
| 50% | 24282.27 | 26248.96 |
| 75% | 32505.66 | 35000.00 |
| max | 328510.97 | 3778206.26 |
В столбце 'price_usd' явных аномалий не увидел. В столбце 'sale_usd' есть отрицательное значение - оно похоже на аномалию. Также надо проверить, есть ли значения ниже минимальной стоимости по столбцу 'price_usd'. Выведу строки со значением ниже минимального в 'price_usd'.
data.query('sale_usd < price_usd.min()')
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13229 | 2019 | 8 | БИПЭК АВТО | Kia | sportage | 2019 | Республика Казахстан | бензин | 2.00 | автомат | 4WD | NaN | NaN | 20700.00 | 0.00 | Карагандинская область | Внедорожники | Компактные SUV |
| 13856 | 2019 | 8 | БИПЭК АВТО | Lada | granta | 2019 | Республика Казахстан | бензин | 1.60 | автомат | FF | NaN | NaN | 7100.00 | 0.00 | г.Алматы | Легковые автомобили | B класс |
| 15164 | 2019 | 8 | БИПЭК АВТО | Lada | xray | 2018 | Республика Казахстан | бензин | 1.60 | механика | FF | NaN | NaN | 13800.00 | 0.00 | Павлодарская область | Легковые автомобили | B класс |
| 20364 | 2019 | 8 | БИПЭК АВТО | Skoda | superb | 2019 | Республика Казахстан | бензин | 2.00 | робот | 4WD | NaN | -1.00 | 35588.25 | -35588.25 | Жамбылская область | Легковые автомобили | D класс |
| 30136 | 2019 | 8 | БИПЭК АВТО | UAZ | 3909 | 2018 | Республика Казахстан | бензин | 2.69 | механика | 4WD | NaN | NaN | 11200.00 | 0.00 | г.Алматы | Коммерческие автомобили | Развозные автомобили |
| 30447 | 2019 | 8 | БИПЭК АВТО | UAZ | patriot | 2018 | Республика Казахстан | бензин | 2.69 | механика | 4WD | NaN | NaN | 13800.00 | 0.00 | Восточно-Казахстанская область | Внедорожники | Среднеразмерные SUV |
| 30908 | 2019 | 8 | БИПЭК АВТО | Skoda | kodiaq | 2019 | Республика Казахстан | бензин | 2.00 | робот | 4WD | NaN | NaN | 40000.00 | 0.00 | г.Нур-Султан | Внедорожники | Полноразмерные SUV |
Есть одна строка с отрицательным значением в столбцах 'sale_usd' и 'quantity'. Похоже на возврат автомобиля. Эту строку можно удалить. Также в 'sale_usd' есть значения 0. В этих строках не заполнены столбцы 'client_type', 'quantity', 'sale_usd'. Данные пропуски заполнить не могу, поэтому строки удаляю.
data = data.query('sale_usd > 0')
data.shape[0]
39959
Удалил 7 строк.
# вывожу уникальные значения в столбце
dubl(data, 'quantity')
Число уникальных значений: 55 Уникальные значения: [ 1. 2. 4. 3. 7. 5. 8. 10. 6. 9. 11. 19. 12. 18. 37. 17. 15. 24. 29. 14. 13. 25. 63. 23. 30. 21. 60. 22. 16. 35. 28. 32. 54. 115. 46. 33. 47. 20. 66. 27. 50. 36. 26. 31. 34. 40. 43. 51. 42. 38. 41. 100. 70. 79. 62.]
В этом столбце проблем уже нет, так как удалил строку с возвратом. Изменю тип столбца на целочисленный.
data.quantity = data.quantity.astype(int)
# переведу все значения в нижний регистр
low(data, 'client_type')
# удаляю пробелы в значениях
data.client_type = data.client_type.replace(r' ', '', regex=True)
# выведу уникальные значения
dubl(data, 'client_type')
Число уникальных значений: 7 Уникальные значения: ['физ.лицо' 'юр.лицо' nan 'физлицо' 'юрлицо' 'корп.клиент' 'юридическое' 'физическое']
# устраняю неявные дубликаты
pattern_1 = ['физ\S+']
pattern_2 = ['юр\S+', 'корп\S+']
re_sub(pattern_1, data, 'client_type', 'физлицо')
re_sub(pattern_2, data, 'client_type', 'юрлицо')
client_nan = {'nan': np.NaN}
implicit_dubl(data, 'client_type', client_nan)
Количество уникальных значений после очистки: 2
# посчитаю, сколько в столбце пропусков
data.client_type.isna().sum()
7040
В столбце 7040 пропусков.
Предполагаю, что физлица обычно покупают не более 1-го автомобиля для личного пользования. Чтобы в этом убедится, посчитаю их долю от всего числа заказов, где количество автомобилей больше одного.
round(len(data.query('client_type == "физлицо" and quantity >= 2'))/len(data.query('quantity >= 2')), 4)
0.0015
Предположение подтвердилось, что физлица очень редко покупают 2 и более автомобилей. Теперь посчитаю, как часто покупают юрлица 1 автомобиль.
round(len(data.query('client_type == "юрлицо" and quantity == 1'))/
len(data.query('quantity == 1 and client_type.notna()')), 2)
0.25
25 % процентов, покупающих 1 автомобиль - это юрлица, поэтому нельзя пропуски заполнить только физлицами в местах, где в заказе 1 автомобиль. Чтобы точнее определить, как заполнить пропуски создам сводную таблицу, которая будет состоять из:
client_nan = (data
.query('quantity == 1')
.groupby(['segmentation', 'client_type'])
.size()
.unstack()
.reset_index().rename_axis(None, axis=1)
)
client_nan['legal_person_count_%'] = round(client_nan['юрлицо']/(client_nan['юрлицо']+client_nan['физлицо'])*100, 2)
client_nan
| segmentation | физлицо | юрлицо | legal_person_count_% | |
|---|---|---|---|---|
| 0 | Внедорожники | 11962 | 2736 | 18.61 |
| 1 | Коммерческие автомобили | 810 | 2653 | 76.61 |
| 2 | Легковые автомобили | 11701 | 2014 | 14.68 |
| 3 | Минивэны | 74 | 35 | 32.11 |
| 4 | Пикапы | 182 | 710 | 79.60 |
Исходя из полученных данных нельзя точно заполнить пропуски, потому что юрлица - заметная доля покупателей в каждом сегменте, особенно это касается коммерческих автомобилей и пикапов. Поэтому пропуски в столбце заполню значением "unknown".
data.loc[data.quantity == 1, 'client_type'] = data.client_type.fillna('unknown')
data.client_type.fillna('юрлицо', inplace=True)
len(data.query('client_type == "unknown"'))
4326
В итоге не удалось заполнить 4326 пропусков в этом столбце 'client_type'.
# вывожу уникальные значения
dubl(data, 'area')
Число уникальных значений: 18 Уникальные значения: ['г.Алматы' 'г.Нур-Султан' 'Восточно-Казахстанская область' 'Западно-Казахстанская область' 'Костанайская область' 'Павлодарская область' 'Южно-Казахстанская область' 'Мангистауская область' 'Атырауская область' 'Актюбинская область' 'Карагандинская область' 'Северо-Казахстанская область' 'Акмолинская область' 'Кызылординская область' 'Жамбылская область' 'Туркестанская область' 'Алматинская область' 'Экспорт область']
В столбце проблем не увидел.
# вывожу уникальные значения
segment = data.segmentation.unique()
segment
array(['Легковые автомобили', 'Внедорожники', 'Коммерческие автомобили',
'Минивэны', 'Пикапы'], dtype=object)
В этом столбце все в порядке.
# вывожу уникальные значения
dubl(data, 'class_2013')
Число уникальных значений: 21 Уникальные значения: ['C класс' 'D класс' 'E класс' 'F класс' 'Среднеразмерные SUV' 'Полноразмерные SUV' 'Спортивные автомобили' 'Компактные SUV' 'B класс' 'Субкомпактные SUV' 'Большие автобусы' 'Крупнотоннажные грузовики' 'Микроавтобусы' 'Малотоннажные грузовики' 'Полноразмерный Минивэн' 'Средние автобусы' 'Среднетоннажные грузовики' 'Pick-ups' 'Развозные автомобили' 'A класс' 'Компактвэн']
Здесь тоже все в порядке.
Для дальнейшего исследования данных создам столбец с месяцем и годом продажи.
data['date_sale'] = data.year.astype('str') + '-' + data.month.astype('str')
data['date_sale'] = pd.to_datetime(data['date_sale'], format='%Y-%m').dt.strftime("%Y-%m")
# считаю количество строк после проведенной предобработки
all_lines_prep = len(data)
print('Количество строк после предобработки:', all_lines_prep)
print('Количество удаленных строк:', all_lines-all_lines_prep)
print('Доля удаленных данных от общего количества данных:', "{:.2%}".format(1-all_lines_prep/all_lines))
print('Kоличество пропусков для каждого столбца в процентах')
display(pd.DataFrame(round(data.isna().mean()*100, 2)).style.background_gradient('coolwarm'))
Количество строк после предобработки: 39959 Количество удаленных строк: 7 Доля удаленных данных от общего количества данных: 0.02% Kоличество пропусков для каждого столбца в процентах
| 0 | |
|---|---|
| year | 0.000000 |
| month | 0.000000 |
| company | 0.000000 |
| brand | 0.000000 |
| model | 0.000000 |
| year_of_release | 0.000000 |
| producing_country | 0.000000 |
| type_of_fuel | 0.000000 |
| engine_volume_l | 4.910000 |
| transmission | 0.000000 |
| type_of_drive | 0.000000 |
| client_type | 0.000000 |
| quantity | 0.000000 |
| price_usd | 0.000000 |
| sale_usd | 0.000000 |
| area | 0.000000 |
| segmentation | 0.000000 |
| class_2013 | 0.000000 |
| date_sale | 0.000000 |
Датасет очищен и подготовлен для дальнейшего анализа.
В ходе предобработки данных сделал:
Выяснил, что:
brand_in_company_kz = data.groupby('company')['brand'].nunique().sort_values(ascending=False).reset_index()
brand_in_company_kz.head()
| company | brand | |
|---|---|---|
| 0 | Allur Auto | 7 |
| 1 | БИПЭК АВТО | 5 |
| 2 | СемАЗ | 4 |
| 3 | Mercur Auto | 3 |
| 4 | Вираж | 3 |
Из таблицы видно, что есть компании делятся на монобрендовые и мультибрендовые. Посчитаю сколько тех и других.
monobrand = brand_in_company_kz.query('brand == 1')['company'].nunique()
multibrand = len(brand_in_company_kz)-monobrand
print('Количество монобрендовых автоцентров:', monobrand)
print('Количество мультибрендовых автоцентров:', multibrand)
Количество монобрендовых автоцентров: 17 Количество мультибрендовых автоцентров: 15
На 2019 год в Казахстане работают 32 автодиллера. Среди них 17 продают один бренд, 15 - несколько.
# строю гистограмму количества брендов у автоцентров в Казахстане на 2019 год
plt.figure(figsize=(15, 8))
plt.hist(brand_in_company_kz.brand, bins=7)
plt.title('Количество брендов у автоцентров в Казахстане на 2019 год')
plt.xlabel('Количество марок')
plt.ylabel('Количество автоцентров')
plt.show();
model_at_brand = data.groupby('brand')['model'].nunique().sort_values(ascending=False).reset_index()
model_at_brand.head()
| brand | model | |
|---|---|---|
| 0 | KAMAZ | 24 |
| 1 | Mercedes-Benz | 19 |
| 2 | Hyundai | 17 |
| 3 | BMW | 14 |
| 4 | Volkswagen | 13 |
Большинство компаний имеют несколько моделей. Больше всего моделей у KAMAZ - 24. Также есть производители, продающие только одну марку.
# расчет количества автоцентров с одной и несколькими моделями
one_model = model_at_brand.query('model == 1')['brand'].nunique()
many_model = len(model_at_brand) - one_model
print('Количество автоцентров с одной моделью:', one_model)
print('Количество автоцентров с несколькими моделями:', many_model)
Количество автоцентров с одной моделью: 5 Количество автоцентров с несколькими моделями: 39
Всего 44 марки автомобилей продают в Казахстане. Из них 5 продают только одну модель, 39 - несколько.
# строю гистограмму количества моделей у брендов в Казахстане в 2019 году
plt.figure(figsize=(15, 8))
plt.hist(model_at_brand.model, bins=24)
plt.title('Количество моделей у брендов в Казахстане в 2019 году')
plt.xlabel('Количество моделей')
plt.ylabel('Количество брендов')
plt.show();
release_of_cars = data.groupby('year_of_release')['quantity'].sum().sort_values(ascending=False).reset_index()
# расчет доли автомобилей по годам
release_of_cars['quantity_ratio'] = round((release_of_cars.quantity/data.quantity.sum()), 3)
release_of_cars
| year_of_release | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | 2019 | 42027 | 0.78 |
| 1 | 2018 | 11419 | 0.21 |
| 2 | 2017 | 394 | 0.01 |
| 3 | 2016 | 30 | 0.00 |
| 4 | 2014 | 2 | 0.00 |
| 5 | 0 | 1 | 0.00 |
| 6 | 2011 | 1 | 0.00 |
| 7 | 2013 | 1 | 0.00 |
В 2019 году продали 42027 машин, выпущенных в 2019 году, что составляет 78 %. 11419 или 21 % - автомобили 2018 года.
manufacturing_countries = data.groupby('producing_country')['quantity'].sum().sort_values(ascending=False).reset_index()
manufacturing_countries['quantity_ratio'] = round((manufacturing_countries.quantity/data.quantity.sum()), 3)
manufacturing_countries.head()
| producing_country | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | Республика Казахстан | 31366 | 0.58 |
| 1 | Российская Федерация | 14341 | 0.27 |
| 2 | Япония | 4612 | 0.09 |
| 3 | Турция | 1167 | 0.02 |
| 4 | Таиланд | 981 | 0.02 |
Больше всего было продано автомобилей произведенных в Казахстане - 58 %. На втором месте Российская Федерация - 27 %. На третьем Япония - 8,6 % количество автомобилей было продано.
sale_by_engine_type = data.groupby('type_of_fuel')['quantity'].sum().sort_values(ascending=False).reset_index()
sale_by_engine_type['quantity_ratio'] = round(sale_by_engine_type.quantity/data.quantity.sum(), 2)
sale_by_engine_type
| type_of_fuel | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | бензин | 51008 | 0.95 |
| 1 | дизель | 2809 | 0.05 |
| 2 | unknown | 34 | 0.00 |
| 3 | электричество | 14 | 0.00 |
| 4 | гибрид | 10 | 0.00 |
95 % рынка приходится на автомобили с бензиновыми двигателями. 5 % - на дизельные двигатели.
Построю диаграмму размаха без учета пропусков.
plt.figure(figsize=(15, 8))
sns.boxplot(data.query('engine_volume_l > 0').engine_volume_l)
plt.title('Распределение автомобилей по объему двигателя')
plt.xlabel('Объем двигателя, л')
plt.show()
print('Минимальный объем двигателя:', data.query('engine_volume_l > 0').engine_volume_l.min())
print('Среднее значение объема двигателя:', round(data.query('engine_volume_l > 0').engine_volume_l.mean(), 2))
print('Максимальный объем двигателя:', data.query('engine_volume_l > 0').engine_volume_l.max())
Минимальный объем двигателя: 1.2 Среднее значение объема двигателя: 2.32 Максимальный объем двигателя: 13.0
На диаграмме видны выбросы. Посмотрю, как будет выглядеть распределение, если удалить выбросы. Удалять буду с помощью межквартильного диапазона.
# посчитаю значения квантилей
q1 = np.quantile(data.query('engine_volume_l > 0').engine_volume_l, .25)
q3 = np.quantile(data.query('engine_volume_l > 0').engine_volume_l, .75)
iqr = q3 - q1
# рассчитываю базовую нижнюю и верхнюю границу диапазона для исключения выбросов
upper_eng = q3 + 1.5 * iqr
lower_eng = q1 - 1.5 * iqr
# создам переменную, в которой будут отброшенные выбросы
not_blowout_eng = data.query('(engine_volume_l > @lower_eng) and (engine_volume_l < @upper_eng) and engine_volume_l > 0')
print('Потери при удалении выбросов:', round((1- len(not_blowout_eng)/data.shape[0]), 4))
Потери при удалении выбросов: 0.0985
# строю даиграмму размаха с отсечением выбросов
plt.figure(figsize=(15, 8))
sns.boxplot(not_blowout_eng.engine_volume_l)
plt.title('Распределение автомобилей по объему двигателя')
plt.xlabel('Объем двигателя, л')
plt.show()
Если удалить выбросы, то потери составят 10 % от всего датасета. Это много, поэтому не удаляю.
# функция для расчета распределений относительно количества автомобилей в заказе
def distrib(df, column_distr, column_regar):
df_distr = df.groupby(column_distr)[column_regar].sum().sort_values(ascending=False).reset_index()
df_distr['quantity_ratio'] = round(df_distr[column_regar]/df[column_regar].sum(), 2)
display(df_distr.head())
distrib(data, 'transmission', 'quantity')
| transmission | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | автомат | 28397 | 0.53 |
| 1 | механика | 17834 | 0.33 |
| 2 | вариатор | 4313 | 0.08 |
| 3 | unknown | 2841 | 0.05 |
| 4 | робот | 490 | 0.01 |
В Казахстане в 2019 году наиболее популярны автомобили с автоматом - 53 %. На втором месте - с механикой - 33%
distrib(data, 'type_of_drive', 'quantity')
| type_of_drive | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | FF | 29347 | 0.54 |
| 1 | 4WD | 18998 | 0.35 |
| 2 | FR | 3730 | 0.07 |
| 3 | AWD | 1057 | 0.02 |
| 4 | unknown | 743 | 0.01 |
Чаще всего покупали в 2019 году технику с передним приводом - 54 %. На втором месте с полным - 35 %.
distrib(data, 'client_type', 'quantity')
| client_type | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | юрлицо | 24807 | 0.46 |
| 1 | физлицо | 24742 | 0.46 |
| 2 | unknown | 4326 | 0.08 |
Физические и юридические лица покупают примерно одинаковое количество автомобилей 46 %. 8 % приходится на строки, где достоверно нельзя указать тип покупателя.
# строю гистограмму количества моделей у брендов в Казахстане в 2019 году
plt.figure(figsize=(15, 8))
plt.hist(data.quantity)
plt.title('Распределение количества автомобилей в одном заказе')
plt.xlabel('Количество')
plt.ylabel('Количество автомобилей')
plt.show();
В основном заказ содержит один автомобиль, намного реже 2-3. Наибольшее число техники в заказе - 115 шт. Интересно посмотреть, как распределяются заказы с большим числом автомобилей по брендам, начиная от 15 штук.
print('Количество продаж:', data.query('quantity >= 15').shape[0])
print(data.query('quantity >= 15')['brand'].unique())
Количество продаж: 248 ['Foton' 'Hyundai' 'Kia' 'Lada' 'MAN' 'Skoda' 'UAZ' 'Урал' 'Daewoo' 'Shacman' 'Dongfeng' 'Jac' 'Volkswagen']
Всего таких продаж 248. Их делали 13 различных бренда.
# диаграмма размаха для распределения цен
plt.figure(figsize=(15, 8))
sns.boxplot(data.price_usd)
plt.title('Распределение цен')
plt.xlabel('Цена, USD')
plt.show()
print('Минимальная цена:', data.query('engine_volume_l > 0').price_usd.min())
print('Средняя цена:', round(data.query('engine_volume_l > 0').price_usd.mean(), 2))
print('Максимальный цена:', data.query('engine_volume_l > 0').price_usd.max())
Минимальная цена: 7100.0 Средняя цена: 27982.4 Максимальный цена: 328510.9706
В основном цена на автомобили на рынке варьируется от 10 до 55 тысяч долларов. На диаграмме видны выбросы. Посчитаю, сколько они занимают от всего датасета.
# посчитаю значения квантилей
q1_price = np.quantile(data.price_usd, .25)
q3_price = np.quantile(data.price_usd, .75)
iqr_price = q3_price - q1_price
# рассчитываю базовую нижнюю и верхнюю границу диапазона для исключения выбросов
upper_price = q3_price + 1.5 * iqr_price
lower_price = q1_price - 1.5 * iqr_price
# создам переменную, в которой будут отброшенные выбросы
price_clean = data.query('price_usd > @lower_price and price_usd < @upper_price')
print('Потери при удалении выбросов:', round(1-(len(price_clean)/len(data)), 3))
Потери при удалении выбросов: 0.071
Если удалить выбросы, то потеряю 7 % данных, а эта потеря может исказить результаты исследования. Оставляю.
distrib(data, 'class_2013', 'quantity')
| class_2013 | quantity | quantity_ratio | |
|---|---|---|---|
| 0 | B класс | 13846 | 0.26 |
| 1 | Субкомпактные SUV | 7681 | 0.14 |
| 2 | Компактные SUV | 7346 | 0.14 |
| 3 | D класс | 5566 | 0.10 |
| 4 | Среднеразмерные SUV | 4401 | 0.08 |
26 % рынка занимает В класс, по 14 % у Субкомпактных и Компактных SUV.
В столбце 'engine_volume_l' и 'price_usd' много разных уникальных значений, что неудобно для анализа, поэтому для дальнейшего анализа их лучше всего категоризировать.
По рабочему объему двигателя автомобили делят на следующие группы:
На основании этих данных проведу категоризацию автомобилей по объему двигателя (столбец engine_volume_l).
volume_labels = ['особо малый', 'малый', 'средний', 'большой']
volume_bins = [0, 1.2, 1.8, 3.5, max(data.engine_volume_l)]
data['engine_class'] = pd.cut(data[data.engine_volume_l !=0].engine_volume_l, bins=volume_bins, labels=volume_labels)
data.head()
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | date_sale | engine_class | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2019 | 5 | Mercur Auto | Audi | a3 | 2018 | Германия | бензин | 1.40 | робот | FF | физлицо | 1 | 28115.00 | 28115.00 | г.Алматы | Легковые автомобили | C класс | 2019-05 | малый |
| 1 | 2019 | 8 | Mercur Auto | Audi | a3 | 2018 | Германия | бензин | 1.40 | робот | FF | юрлицо | 1 | 32246.99 | 32246.99 | г.Алматы | Легковые автомобили | C класс | 2019-08 | малый |
| 2 | 2019 | 4 | Mercur Auto | Audi | a4 | 2018 | Германия | бензин | 1.40 | робот | FF | физлицо | 1 | 32000.00 | 32000.00 | г.Алматы | Легковые автомобили | D класс | 2019-04 | малый |
| 3 | 2019 | 7 | Mercur Auto | Audi | a4 | 2018 | Германия | бензин | 1.40 | робот | FF | юрлицо | 1 | 31929.00 | 31929.00 | г.Алматы | Легковые автомобили | D класс | 2019-07 | малый |
| 4 | 2019 | 7 | Mercur Auto | Audi | a4 | 2018 | Германия | бензин | 1.40 | робот | FF | физлицо | 1 | 31929.00 | 31929.00 | г.Алматы | Легковые автомобили | D класс | 2019-07 | малый |
Категоризацию столбца проведу на основании очищенного от выбросов датасета. Он хранится в переменной not_blowout_eng. Значения столбца разделю на четыре равные части по квартилям.
volume_bins = [[0], stat.quantiles(not_blowout_eng.price_usd, n=4, method='exclusive'), [max(data.price_usd)]]
# список списков перевожу в обычный список
volume_bins = list(itertools.chain(*volume_bins))
volume_bins
[0, 15261.97794, 22709.4814, 30993.33102, 328510.9706]
На основании полученных данных создам следующие сегменты по цене:
volume_labels = ['низкая', 'средняя', 'выше среднего', 'высокая']
data['price_cat'] = pd.cut(data.price_usd, bins=volume_bins, labels=volume_labels)
data.head()
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | date_sale | engine_class | price_cat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2019 | 5 | Mercur Auto | Audi | a3 | 2018 | Германия | бензин | 1.40 | робот | FF | физлицо | 1 | 28115.00 | 28115.00 | г.Алматы | Легковые автомобили | C класс | 2019-05 | малый | выше среднего |
| 1 | 2019 | 8 | Mercur Auto | Audi | a3 | 2018 | Германия | бензин | 1.40 | робот | FF | юрлицо | 1 | 32246.99 | 32246.99 | г.Алматы | Легковые автомобили | C класс | 2019-08 | малый | высокая |
| 2 | 2019 | 4 | Mercur Auto | Audi | a4 | 2018 | Германия | бензин | 1.40 | робот | FF | физлицо | 1 | 32000.00 | 32000.00 | г.Алматы | Легковые автомобили | D класс | 2019-04 | малый | высокая |
| 3 | 2019 | 7 | Mercur Auto | Audi | a4 | 2018 | Германия | бензин | 1.40 | робот | FF | юрлицо | 1 | 31929.00 | 31929.00 | г.Алматы | Легковые автомобили | D класс | 2019-07 | малый | высокая |
| 4 | 2019 | 7 | Mercur Auto | Audi | a4 | 2018 | Германия | бензин | 1.40 | робот | FF | физлицо | 1 | 31929.00 | 31929.00 | г.Алматы | Легковые автомобили | D класс | 2019-07 | малый | высокая |
Наличие зависимостей между признаками в датасете выбираю следующие: 'year_of_release', 'type_of_fuel', 'engine_class', 'sale_usd', 'transmission', 'client_type', 'type_of_drive', 'segmentation'.
Для проведения корреляционного анализа подготовлю данные:
# удаляю пропуски
corr = data.query('year_of_release != 0 and engine_volume_l != 0 and type_of_fuel != "unknown" and \
transmission != "unknown" and type_of_drive != "unknown" and engine_class != "0" and client_type != "unknown"')
corr = data[['year_of_release', 'type_of_fuel', 'engine_class', \
'transmission', 'client_type', 'type_of_drive', 'price_cat', 'segmentation']]
# кодирую категориальные признаки числами
corr.type_of_fuel = corr.type_of_fuel.map({'бензин': 1, 'дизель': 2, 'гибрид': 3, 'электричество': 4})
corr.engine_class = corr.engine_class.map({'особо малый': 1, 'малый': 2, 'средний': 3, 'большой': 4})
corr.transmission = corr.transmission.map({'робот': 1, 'автомат': 2, 'механика': 3, 'вариатор': 4})
corr.type_of_drive = corr.type_of_drive.map({'FF': 1, '4WD': 2, 'FR': 3, 'AWD': 4})
corr.client_type = corr.client_type.map({'физлицо': 1, 'юрлицо': 2})
segment_change_dict = {n: i for n, i in zip(corr.segmentation.unique().tolist(), list(range(1, 6)))}
corr.segmentation = corr.segmentation.map(segment_change_dict)
Поскольку в данных присутствуют категориальные переменные для поиска зависимости буду использовать коэффициент корреляции Phi_K.
# строю матрицу корреляции
plt.figure(figsize=(10, 6))
plt.title('Корреляция признаков по коэффициенту Phi_K')
sns.heatmap(corr.phik_matrix(verbose=False), linewidths=2, square=True, cbar=False, cmap='Greens', annot=True);
Для оценки зависимостей использовал шкалу Чеддока. На диаграмме наблюдается:
Рынок автомобилей делится на коммерческие и некоммерческие. Поэтому в этом разрезе и буду смотреть на продажи. Но для начала посчитаю полную емкость рынка.
Емкость рынка в количественном выражении - общий объем реализованной продукции за период. Поэтому чтобы рассчитать емкость рынка в количественном выражении, надо сложить все значения в столбце 'quantity' (количество автомобилей в заказе).
size_quantity = data.quantity.sum()
print('Емкость рынка в количественном выражении с января по сентябрь в 2019 году в Казахстане составила', \
size_quantity, 'автомобилей.')
Емкость рынка в количественном выражении с января по сентябрь в 2019 году в Казахстане составила 53875 автомобилей.
Далее посчитаю какова предполагаемая емкость за весь 2019 год. За предыдущие 10 лет емкость продаж в четвертом квартале составляла 30-50 % от общегодового. Поэтому для прогноза возьму усредненный показатель в 40 %.
forecast_size_quantity = round(data.quantity.sum() + (data.quantity.sum() * 0.4))
print('Прогнозируемая количественная емкость составляет', \
forecast_size_quantity, 'автомобилей.')
Прогнозируемая количественная емкость составляет 75425 автомобилей.
Теперь посчитаю емкость продаж в денежном выражении за 9 месяцев и прогнозируемую за весь год. Для этого сложу все цены заказа в столбце 'sale_usd'.
size_sale = data.sale_usd.sum()
print(f'Емкость в денежном выражении за 9 месяцев составляет \
{size_sale:,.2f} доллара'.replace(',', ' '))
forecast_size_sale = (data.sale_usd.sum() + data.sale_usd.sum() * 0.4)
print(f'Прогнозируемая емкость в денежном выражении за 2019 год составляет \
{forecast_size_sale:,.2f} доллара'.replace(',', ' '))
Емкость в денежном выражении за 9 месяцев составляет 1 349 194 556.31 доллара Прогнозируемая емкость в денежном выражении за 2019 год составляет 1 888 872 378.83 доллара
Рассчитаю емкость в денежном и количественном выражениях за 9 месяцев 2019 года.
commercial_quantity = round(data.query('segmentation == "Коммерческие автомобили"').quantity.sum())
print('Емкость рынка в количественном выражении составила', commercial_quantity, 'коммерческих автомобилей.')
commercial_sale = data[data.segmentation == "Коммерческие автомобили"].sale_usd.sum()
print(f'Емкость рынка в денежном выражении составила \
{commercial_sale:,.2f} доллара коммерческих автомобилей.'.replace(',',' '))
Емкость рынка в количественном выражении составила 5246 коммерческих автомобилей. Емкость рынка в денежном выражении составила 185 811 934.25 доллара коммерческих автомобилей.
Рассчитаю предполагаемую емкость в денежном и количественном выражениях на 2019 год.
comm_quant_forecast = round(commercial_quantity + commercial_quantity * 0.4)
print('Прогнозируемая емкость рынка в количественном выражении составит', \
comm_quant_forecast, 'коммерческих автомобиля.')
comm_sale_forecast = commercial_sale + commercial_sale * 0.4
print(f'Прогнозируемая емкость рынка в денежном выражении составит \
{comm_sale_forecast:,.2f} доллара для коммерческих автомобилей.'.replace(',',' '))
Прогнозируемая емкость рынка в количественном выражении составит 7344 коммерческих автомобиля. Прогнозируемая емкость рынка в денежном выражении составит 260 136 707.96 доллара для коммерческих автомобилей.
Рассчитаю емкость рынка в денежном и количественном выражениях за 9 месяцев 2019 года.
nocommercial_quantity = size_quantity - commercial_quantity
print('Емкость рынка в количественном выражении составила', nocommercial_quantity, 'некоммерческих автомобилей.')
nocommercial_sale = size_sale - commercial_sale
print(f'Емкость рынка в денежном выражении составила \
{nocommercial_sale:,.2f} доллара для некоммерческих автомобилей.'.replace(',',' '))
Емкость рынка в количественном выражении составила 48629 некоммерческих автомобилей. Емкость рынка в денежном выражении составила 1 163 382 622.05 доллара для некоммерческих автомобилей.
Рассчитаю прогнозируемую емкость на 2019 год.
nocomm_quant_forecast = forecast_size_quantity - comm_quant_forecast
print('Прогнозируемая емкость рынка в количественном выражении составит', \
nocomm_quant_forecast, 'некоммерческих автомобилей.')
nocomm_sale_forecast = forecast_size_sale - comm_sale_forecast
print(f'Прогнозируемая емкость рынка в денежном выражении составит \
{nocomm_sale_forecast:,.2f} доллара для некоммерческих автомобилей.'.replace(',',' '))
Прогнозируемая емкость рынка в количественном выражении составит 68081 некоммерческих автомобилей. Прогнозируемая емкость рынка в денежном выражении составит 1 628 735 670.88 доллара для некоммерческих автомобилей.
# размещение диаграмм рядом
fig = make_subplots(rows=1, cols=2, specs=[[{"type": "pie"}, {"type": "pie"}]],
subplot_titles=['Количественная емкость рынка', 'Емкость рынка в USD'])
# диаграмма для количественной емкости рынка
fig.add_trace(go.Pie(labels=['коммерческий', 'некоммерческий'],
values= [commercial_quantity, nocommercial_quantity],
texttemplate = "%{percent: .1%}",
hole=.4,
domain=dict(x=[0.5, 0.5]),
name=""),
row=1, col=1)
# диаграмма для емкости рынка в денежном выражении
fig.add_trace(go.Pie(labels=['коммерческий', 'некоммерческий'],
values= [commercial_sale, nocommercial_sale],
texttemplate = "%{percent: .1%}",
hole=.4,
domain=dict(x=[1, 1]),
name=""),
row=1, col=2)
# добавляю название
fig.update_layout(
title="Доли коммерческого и некоммерческого транспорта на рынке в Казахстане в 2019 году",
title_x = 0.45)
fig.show()
Коммерческий транспорт в 2019 году в Казахстане занимал 9,7 % в количественном выражении и 13,8 % в денежном. Некоммерческий транспорт 90,3 % и 86,2 % соответственно.
За 9 первых месяцев 2019 года в Казахстане было продано 53875 автомобиля из них 5246 или 9,7 % приходится на коммерческий транспорт, 90,3 % или 48629 автомобилей на некоммерческий транспорт. Выручка со всех продаж составила 1 миллиард 349 миллионов долларов. 185 миллионов 811 тысяч или 13,8 % приходится на коммерческий транспорт, 86,2 % или 1 миллиард 163 миллиона - на некоммерческий.
Чтобы определить марки-лидеры, сгруппирую датасет по ним и просуммирую количество техники в заказе и стоимость заказа.
# топ лидеров в общем на рынке по продажам в денежном эквиваленте
brand_top_sale = (data
.groupby('brand')['sale_usd']
.sum()
.sort_values(ascending=False)
.reset_index()
)
# топ лидеров в общем на рынке по продажам по количеству техники
brand_top_quantity = (data
.groupby('brand')['quantity']
.sum()
.sort_values(ascending=False)
.reset_index()
)
# код для отображения двух фреймов рядом
df1_styler = (brand_top_sale
.head(10)
.style
.set_table_attributes("style='display:inline'")
.set_caption('Топ-10 брендов автомобилей по продажам в USD')
)
df2_styler = (brand_top_quantity
.head(10)
.style
.set_table_attributes("style='display:inline'")
.set_caption('Топ-10 брендов по количеству проданных автомобилей')
)
space = "\xa0" * 20 # создаю промежуток между фреймами
display_html(df1_styler._repr_html_()+ space + df2_styler._repr_html_(), raw=True)
| brand | sale_usd | |
|---|---|---|
| 0 | Toyota | 402500067.912330 |
| 1 | Hyundai | 239018204.781610 |
| 2 | Lada | 107847478.947374 |
| 3 | Lexus | 77376371.520560 |
| 4 | KAMAZ | 46092683.388360 |
| 5 | Kia | 45669700.000000 |
| 6 | Nissan | 40012772.489470 |
| 7 | GAZ | 32286199.162010 |
| 8 | Jac | 30589381.108639 |
| 9 | ANKAI | 28907055.438830 |
| brand | quantity | |
|---|---|---|
| 0 | Lada | 11092 |
| 1 | Toyota | 10745 |
| 2 | Hyundai | 10404 |
| 3 | Kia | 2358 |
| 4 | Jac | 2099 |
| 5 | GAZ | 1821 |
| 6 | Renault | 1796 |
| 7 | Ravon | 1795 |
| 8 | Nissan | 1664 |
| 9 | UAZ | 1361 |
# сртою столбчатую диаграмму
fig = px.bar(brand_top_sale.head(10),
x='sale_usd',
y='brand',
color = 'brand',
hover_name="brand"
)
# добавляю название и подписи осей
fig.update_layout(
title="Топ-10 брендов автомобилей по продажам в USD",
xaxis_title='Продажи, USD',
yaxis_title='Бренд',
title_x = 0.5
)
fig.show()
# сртою столбчатую диаграмму
fig = px.bar(brand_top_quantity.head(10),
x='quantity',
y='brand',
color = 'brand',
hover_name="brand"
)
# добавляю название и подписи осей
fig.update_layout(
title="Топ-10 брендов по количеству проданных автомобилей",
xaxis_title='Продажи, шт.',
yaxis_title='Бренд',
title_x = 0.5
)
fig.show()
По количеству вырученных денег с большим отрывом лидирует Toyota, на втором месте Hyundai, на третьем Lada. По количеству проданных автомобилей рейтинг выглядит иначе. Lada на первом месте, 2-е у Toyota, 3-е Hyundai. Разрыв между топ-3 не сильный.
Расположение брендов с 4-го по 10-е место сильно отличаются:
# топ лидеров в общем на рынке по продажам в денежном эквиваленте
brand_top_sale_commerce = (data
.query('segmentation == "Коммерческие автомобили"')
.groupby('brand')['sale_usd', 'quantity']
.sum()
.sort_values('sale_usd', ascending=False)
.reset_index()
)
# топ лидеров в общем на рынке по продажам по количеству техники
brand_top_quant_nocommerce = (data
.query('segmentation != "Коммерческие автомобили"')
.groupby('brand')['sale_usd', 'quantity']
.sum()
.sort_values('sale_usd', ascending=False)
.reset_index()
)
# код для отображения двух фреймов рядом
df1_styler_comm = (brand_top_sale_commerce
.head(10)
.style
.set_table_attributes("style='display:inline'")
.set_caption('Топ-10 брендов по продажам коммерческих автомобилей')
)
df2_styler_noncom = (brand_top_quant_nocommerce
.head(10)
.style
.set_table_attributes("style='display:inline'")
.set_caption('Топ-10 брендов по продажам некоммерческих автомобилей')
)
space = "\xa0" * 20 # создаю промежуток между фреймами
display_html(df1_styler_comm._repr_html_()+ space + df2_styler_noncom._repr_html_(), raw=True)
| brand | sale_usd | quantity | |
|---|---|---|---|
| 0 | KAMAZ | 46092683.388360 | 892 |
| 1 | GAZ | 32286199.162010 | 1821 |
| 2 | ANKAI | 28907055.438830 | 300 |
| 3 | Daewoo | 12162701.935390 | 146 |
| 4 | UAZ | 11248278.170553 | 844 |
| 5 | Hyundai | 9710608.677650 | 236 |
| 6 | MAN | 7799873.323200 | 69 |
| 7 | Shacman | 5468192.145900 | 101 |
| 8 | Dongfeng | 4374488.400940 | 160 |
| 9 | Mercedes-Benz | 4271360.708800 | 56 |
| brand | sale_usd | quantity | |
|---|---|---|---|
| 0 | Toyota | 402500067.912330 | 10745 |
| 1 | Hyundai | 229307596.103960 | 10168 |
| 2 | Lada | 107847478.947374 | 11092 |
| 3 | Lexus | 77376371.520560 | 1206 |
| 4 | Kia | 45669700.000000 | 2358 |
| 5 | Nissan | 40012772.489470 | 1664 |
| 6 | BMW | 28735768.370860 | 438 |
| 7 | Jac | 26595338.191695 | 1907 |
| 8 | Volkswagen | 25149673.093390 | 1287 |
| 9 | Renault | 24184906.234080 | 1796 |
# размещение диаграмм рядом
fig = make_subplots(rows=1, cols=2, specs=[[{"type": "pie"}, {"type": "pie"}]],
subplot_titles=['Коммерческий транспорт', 'Некоммерческий транспорт'])
# диаграмма для количественной емкости рынка
fig.add_trace(go.Pie(labels=brand_top_sale_commerce['brand'].head(10),
values= brand_top_sale_commerce['sale_usd'],
texttemplate = "%{label} <br>%{percent: .1%}",
hole=.4,
domain=dict(x=[0.5, 0.5]),
),
row=1, col=1)
# диаграмма для емкости рынка в денежном выражении
fig.add_trace(go.Pie(labels=brand_top_quant_nocommerce['brand'].head(10),
values= brand_top_quant_nocommerce['sale_usd'],
texttemplate = "%{label} <br>%{percent: .1%}",
hole=.4,
domain=dict(x=[1, 1])
),
row=1, col=2)
# добавляю название
fig.update_layout(
title="Доли марок на рынке в Казахстане в 2019 году",
title_x = 0.45,
showlegend=False
)
fig.show()
Коммерческий сегмент
Среди коммерческого транспорта первое место на рынке 28,4 % занимает Камаз, на втором GAZ - 19,9 %. В Казахстане юрлица предпочитают покупать автомобили из России. Третье место с 17,8 % у китайского бренда ANKAI. Эти три марки занимают почти 66 % рынка. 55 % рынка занимают Российские производители транспорта.
Некоммерческий сегмент
В нем лидируют японский бренд Toyota, который занимает 40 % потребительского рынка, далее идет Hyundai с 22,8 %. Третье место Lada - 10,7 %. Эти три марки захватили 73 % всего рынка. Японские производители занимают 56 % рынка.
# создаю таблицу с изменением цены по месяцам
dynamics_graph = data.groupby('date_sale').sale_usd.sum().reset_index()
# строю график общей динамики рынка
plt.figure(figsize=(15, 8))
plt.plot(dynamics_graph.date_sale,
dynamics_graph.sale_usd,
linewidth=2,
marker='s')
# добавление значений на график
for x, y, tex in zip(dynamics_graph.date_sale, dynamics_graph.sale_usd, dynamics_graph.sale_usd):
t = plt.text(x, y, int(tex/1000),
horizontalalignment='right',
rotation='horizontal',
verticalalignment='bottom',
fontdict={'color':'black', 'weight':'bold'})
plt.title('Общая динамика продаж автомобилей в Казахстане в 2019 году (тыс.USD)')
plt.xlabel('Месяц продажи')
plt.ylabel('Объем продаж, USD')
# убираю ось Y
ax = plt.gca()
ax.get_yaxis().set_visible(False)
plt.show()
На графике видно, что начиная с марта до мая объем продаж автомобилей рос. Далее в июне он вышел на плато, а затем резкий рост в июле, за которым последовал такой же резкий спад.
Чтобы получить дополнительную информацию, разобью график объема продаж на коммерческий и некоммерческий транспорт.
dynamics_graph_commers = (data
.query('segmentation == "Коммерческие автомобили"')
.groupby('date_sale').sale_usd.sum().reset_index()
)
dynamics_graph_nocommers = (data
.query('segmentation != "Коммерческие автомобили"')
.groupby('date_sale').sale_usd.sum().reset_index()
)
# строю график общей динамики рынка
plt.figure(figsize=(15, 8))
plt.plot(dynamics_graph_commers.date_sale,
dynamics_graph_commers.sale_usd,
linewidth=2,
marker='s',
label='Динамика продаж коммерческого транспорта'
)
plt.plot(dynamics_graph_nocommers.date_sale,
dynamics_graph_nocommers.sale_usd,
linewidth=2,
marker='s',
label='Динамика продаж некоммерческого транспорта'
)
plt.title('Динамика продаж автомобилей в Казахстане в 2019 году в USD')
plt.xlabel('Месяц продажи')
plt.ylabel('Объем продаж, USD')
plt.legend(loc="upper left")
plt.show()
Выводы
# функция для графиков динамики продаж
def dynamics(df, index, columns, values, aggfunc, typ):
df = (df
.pivot_table(index=index,
columns=columns,
values=values,
aggfunc=aggfunc)
)
# устанавливаю размеры графиков
color = ['green', 'b', 'r', 'black', 'y', 'm', 'c']
df.plot(figsize=(15, 8), marker='s', color=color)
plt.xlabel('Месяц продажи')
plt.ylabel('Объем продаж, USD')
plt.title('Динамика объема продаж автомобилей по {}'.format(typ))
plt.show()
# создам таблицу с группировкой по сегментам и месяцам
dynamics_by_segment = data.groupby(['segmentation', 'date_sale']).sale_usd.sum().reset_index()
dynamics(dynamics_by_segment, 'date_sale', 'segmentation', 'sale_usd', 'sum', 'сегментам')
В целом этот график повторяет график с разделением на коммерческий и некоммерческий транспорт. Также на нем видно, что покупатели в Казахстане предпочитают внедорожники.
Поскольку корреляционный анализ показал высокую взаимосвязь сегмента с типом привода, а также умеренную с типом топлива и с объемом двигателя, то для этих показателей тоже выведу динамическую характеристику. Предполагаю, что все они помогут выявить тип автомобиля, который чаще всего покупают в Казахстане.
Динамика продаж по типу привода
dyn_by_type_drive = (data.query('type_of_drive != "unknown"')
.groupby(['type_of_drive', 'date_sale']).sale_usd.sum().reset_index()
)
dynamics(dyn_by_type_drive, 'date_sale', 'type_of_drive', 'sale_usd', 'sum', 'типу привода')
Видно, что на рынке лидируют автомобили с полным приводом.
Динамика продаж по типу топлива
dyn_by_type_fuel = (data.query('type_of_fuel != 0')
.groupby(['type_of_fuel', 'date_sale']).sale_usd.sum().reset_index()
)
dynamics(dyn_by_type_fuel, 'date_sale', 'type_of_fuel', 'sale_usd', 'sum', 'по типу топлива')
Наибольшая популярность у техники с бензиновым двигателем. Гибридные и электрические автомобили редкость для Казахстана.
Динамика продаж по объему двигателя
dyn_by_eng_class = (data.groupby(['engine_class', 'date_sale']).sale_usd.sum().reset_index())
dynamics(dyn_by_eng_class, 'date_sale', 'engine_class', 'sale_usd', 'sum', 'объему двигателя')
Видно, что чаще всего покупают автомобили со среднем объемом двигателя - от 1,8 до 3,5 л.
Вывод
Исходя из графиков, можно сделать вывод, что в Казахстане чаще всего покупают полноприводные бензиновые внедорожники с объемом двигателя от 1,8 до 3,5.
Тепрь выясню, какие модели внедорожников продавались продавались чаще всего и какие принесли больше выручку.
# посчитаю количество купленных автомобилей
data.query('segmentation == "Внедорожники"').groupby(['brand', 'model'])['quantity'].sum().nlargest(10).reset_index()
| brand | model | quantity | |
|---|---|---|---|
| 0 | Hyundai | tucson | 3271 |
| 1 | Lada | 4x4 | 2469 |
| 2 | Hyundai | creta | 1775 |
| 3 | Toyota | lc prado | 1711 |
| 4 | Jac | s3 | 1355 |
| 5 | Toyota | rav4 | 1213 |
| 6 | Toyota | lc200 | 779 |
| 7 | Renault | duster | 707 |
| 8 | Hyundai | santa fe | 639 |
| 9 | Nissan | qashqai | 601 |
# посчитаю выручку
data.query('segmentation == "Внедорожники"').groupby(['brand', 'model'])['sale_usd'].sum().nlargest(10).reset_index()
| brand | model | sale_usd | |
|---|---|---|---|
| 0 | Hyundai | tucson | 87315649.16 |
| 1 | Toyota | lc prado | 83134112.05 |
| 2 | Toyota | lc200 | 57893775.93 |
| 3 | Hyundai | creta | 35222266.67 |
| 4 | Toyota | rav4 | 33725128.08 |
| 5 | Lexus | lx | 33640569.08 |
| 6 | Lexus | rx | 24363857.89 |
| 7 | Hyundai | santa fe | 23288661.43 |
| 8 | Lada | 4x4 | 22338600.00 |
| 9 | Jac | s3 | 17847414.92 |
По количеству и наибольшей выручке лидирует Hyundai Tucson. Если смотреть на предыдущие графики, то, скорее всего, чаще всего его покупали в такой комплектации: бензиновый двигатель, полный привод и объем двигателя от 1,8 до 3,5. Интересно, что по количеству проданных автомобилей Lada 4x4 (Niva) попала на 1 место, в то время, как по выручке всего на 8-м.
Поскольку высокая корреляция наблюдается между сегментом и типом привода, построю тепловую карту продаж по сегментам в зависимости от типа привода. И эти данные сопоставлю с количеством конкурентов, разбившихся по сегментам.
# продажи по сегментам в зависимости от типа привода
segment_tpdrive = (data
.query('type_of_drive != "unknown"')
.pivot_table(index='segmentation',
columns='type_of_drive',
values='sale_usd',
aggfunc='sum')
)
# число компаний по сегментам в зависимости от типа привода
segment_opponent = (data
.query('type_of_drive != "unknown"')
.pivot_table(index='segmentation',
columns='type_of_drive',
values='company',
aggfunc='nunique')
)
plt.figure(figsize=(8, 5))
ax1 = plt.subplot(2, 1, 1)
plt.title('Продажи по сегментам в зависимости от типа привода, USD')
sns.heatmap(segment_tpdrive, linewidths=2, fmt=',.0f', cbar=False, cmap='Greens', annot=True, ax=ax1)
ax2 = plt.subplot(2, 1, 2)
plt.title('Число компаний в сегментах в зависимости от типа привода')
sns.heatmap(segment_opponent, linewidths=2, fmt=',.0f', cbar=False, cmap='Greens', annot=True, ax=ax2)
plt.subplots_adjust(hspace = 0.6)
plt.show()
На первой матрице видно, что наибольшую прибыль приносит сегмент внедорожников с полным приводом. На втором месте идут легковые автомобили с передним приводом. На третьем внедорожники также с передним приводом.
Ничего удивительного, что в самых прибыльных сегментах очень серьезная конкуренция. Это хорошо видно не втором графике.
Еще интересно, что минивэнов с передним и роботизированным приводами нет на рынке в Казахстане.
Посмотрю, как распределяются продажи среди топ-10 брендов по сегментам.
# создаю сводную таблицу сгруппированную по бренду
brand_segment = (data.query('brand in @brand_top_sale.brand.head(10)')
.groupby(['brand', 'segmentation'])
.sale_usd
.sum()
.unstack()
.reset_index().rename_axis(None, axis=1)
)
# строю столбчатую диаграмму обема продаж автомобилей в разрезе бренд/сегмент
fig = px.bar(brand_segment,
x=segment,
y='brand',
barmode='stack'
)
fig.update_layout(
title="Обем продаж автомобилей в разрезе бренд/сегмент, USD",
xaxis_title='Объем продаж, USD',
yaxis_title='Бренд',
yaxis={'categoryorder':'total ascending'},
legend_title_text='Сегмент',
title_x = 0.5,
width=1000,
height=500
)
fig.update_traces(textposition='inside')
fig.show()
В топ-10 по объему продаж автомобилей в разрезе бренд/сегмент видно:
Ради интереса посмотрю, как у брендов разбивается выручка по типу топлива.
type_of_fuel = data.query('type_of_fuel != "unknown"').type_of_fuel.unique()
# подготовка данных для создания графика
brand_fuel = (data.query('brand in @brand_top_sale.brand.head(10)')
.groupby(['brand', 'type_of_fuel'])
.sale_usd.sum()
.unstack()
.reset_index().rename_axis(None, axis=1)
)
fig = px.bar(brand_fuel,
x=type_of_fuel,
y='brand',
barmode='stack'
)
fig.update_layout(
title="Объем продаж автомобилей в разрезе бренд/тип топлива, USD",
xaxis_title='Объем продаж, USD',
yaxis_title='Бренд',
yaxis={'categoryorder':'total ascending'},
legend_title_text='Топливо',
title_x = 0.5,
width=1000,
height=500
)
fig.update_traces(textposition='inside')
fig.show()
Большинство продаж приходится на бензиновые двигатели. Причем у Toyota, Lada, Lexus, Kia, Nizzan продаются только авто с бензиновым двигателем. Камаз единственная компания, которая продает технику только с дизельным двигателем. Также интересно, что в топ-10 попал бренд ANKAI, который поставляет технику с дизельными и электрическими двигателями.
comm_nocomm = (data
.groupby(['brand', 'segmentation'])['sale_usd'].sum()
.unstack()
.reset_index()
.rename_axis(None, axis=1)
.fillna(0)
)
comm_nocomm['Некоммерческие автомобили'] = comm_nocomm\
.loc[:,['Внедорожники', 'Легковые автомобили', 'Пикапы', 'Минивэны', 'Минивэны']].sum(axis=1)
comm_nocomm.drop(columns=['Внедорожники', 'Легковые автомобили', 'Пикапы', 'Минивэны', 'Минивэны'], inplace=True)
fig = px.bar(comm_nocomm,
x='brand',
y=['Коммерческие автомобили', 'Некоммерческие автомобили'],
barmode='group'
)
fig.update_layout(
title="Продажи по маркам в разрезе коммерческого и некоммерческого транспорта, USD",
xaxis_title='Бренд',
yaxis_title='Объем продаж, USD',
xaxis={'categoryorder':'total descending'},
legend_title_text='',
title_x = 0.5,
width=950,
height=700
)
fig.show()
Большинство производителей специализируются на одном типе автомобилей коммерческие или некоммерческие. Но есть и те, кто производит оба типа, например, Hyundai, Jac, UAZ.
Для начала посчитаю, сколько выручили денег автоцентры по каждому региону в общем.
sale_by_region = data.groupby('area')['sale_usd', 'quantity'].sum().reset_index()
# график общей выручки по регионам
sale_by_region = sale_by_region.sort_values(by='sale_usd', ascending=False)
plt.figure(figsize=(15, 8))
plt.bar(sale_by_region.area, sale_by_region.sale_usd)
for x, y, tex in zip(sale_by_region.area, sale_by_region.sale_usd, sale_by_region.sale_usd):
t = plt.text(x, y, int(tex/1000),
horizontalalignment='center',
rotation='horizontal',
verticalalignment='bottom',
fontdict={'color':'black', 'weight':'bold'})
plt.title('Общая выручка с продажи автомобилей в разрезе регионов Казахстана в 2019 году (тыс. USD)')
plt.xticks(rotation=90)
ax = plt.gca()
ax.get_yaxis().set_visible(False)
plt.show()
За 9 месяцев больше всего заработали автоцентров в г. Алматы и г. Нур-Султан. Меньше всего покупали автомобили в Туркестанской области. Теперь посмотрю продажи в разрезе коммерческого и некоммерческого транспорта.
comm_nocomm_area = (data
.groupby(['area', 'segmentation'])['sale_usd'].sum()
.unstack()
.reset_index()
.rename_axis(None, axis=1)
.fillna(0)
)
comm_nocomm_area['Некоммерческие автомобили'] = comm_nocomm_area\
.loc[:,['Внедорожники', 'Легковые автомобили', 'Пикапы', 'Минивэны', 'Минивэны']].sum(axis=1)
comm_nocomm_area.drop(columns=['Внедорожники', 'Легковые автомобили', 'Пикапы', 'Минивэны', 'Минивэны'], inplace=True)
# график для сравнения продаж в разрезе коммерческих и некоммерческих автомобилей
fig = px.bar(comm_nocomm_area,
x='area',
y=['Коммерческие автомобили', 'Некоммерческие автомобили'],
barmode='group'
)
# добавляю название и подписи осей
fig.update_layout(
title="Продажи по маркам в разрезе коммерческого и некоммерческого транспорта, USD",
xaxis_title='Область',
yaxis_title='Объем продаж, USD',
xaxis={'categoryorder':'total descending'},
legend_title_text='',
title_x = 0.5,
width=1000,
height=800
)
fig.show()
Город Алматы лидирует в продажах и коммерческой и некоммерческой техники, что не удивительно - это самый большой город в Казахстане по населению. На втором месте идет столица Нур-Султан, теперь Астана.
Посмотрю, как по регионам распределялась средняя стоимость продажи.
# добавляю столбец средней продажи по регионам
sale_by_region['mean_sale'] = sale_by_region.sale_usd / sale_by_region.quantity
# график средней цены продажи по регионам
sale_by_region = sale_by_region.sort_values(by='mean_sale', ascending=False)
plt.figure(figsize=(15, 8))
plt.bar(sale_by_region.area, sale_by_region.mean_sale)
for x, y, tex in zip(sale_by_region.area, sale_by_region.mean_sale, sale_by_region.mean_sale):
t = plt.text(x, y, int(tex),
horizontalalignment='center',
rotation='horizontal',
verticalalignment='bottom',
fontdict={'color':'black', 'weight':'bold'})
plt.title('Средняя стоимость продажи автомобилей в разрезе регионов Казахстана в 2019 году')
plt.xticks(rotation=90)
ax = plt.gca()
ax.get_yaxis().set_visible(False)
plt.show()
В Туркестанской области наибольшая средняя стоимость продажи автомобилей, на 14 тыс. долларов больше, чем в г. Алматы. При этом в ней покупают меньше всего 19 автомобилей за 9 месяцев. Возможно, люди ездят покупать автомобили в другие области или в соседний Узбекистан, так как в ней мало диллерских центров или только с люксовыми автомобилями.
Посмотрю динамику продаж по топ-5 регионам.
top_5_reg = sale_by_region.sort_values('sale_usd', ascending=False).head(5)
area_sale_date = data.groupby(['area', 'date_sale'])['sale_usd', 'quantity'].sum().reset_index()
area_sale_date_t_5 = area_sale_date.query('area in @top_5_reg.area')
dynamics(area_sale_date_t_5, 'date_sale', 'area', 'sale_usd', 'sum', 'регионам')
Похоже, что на общую динамику продаж значительно повлияли продажи в Костанайской боласти и незначительно в г. Нур-Султане. Посмотрю, как в динамике менялась средняя стоимось автомобилей в этих 5-ти регионах.
area_sale_date['mean_sale'] = area_sale_date.sale_usd / area_sale_date.quantity
top_5_reg_mean = sale_by_region.sort_values('mean_sale', ascending=False).head(5)
top_5_reg_mean
| area | sale_usd | quantity | mean_sale | |
|---|---|---|---|---|
| 13 | Туркестанская область | 852711.16 | 19 | 44879.53 |
| 16 | г.Алматы | 426474005.34 | 13947 | 30578.19 |
| 17 | г.Нур-Султан | 292675683.31 | 10158 | 28812.33 |
| 8 | Костанайская область | 88334490.44 | 3305 | 26727.53 |
| 1 | Актюбинская область | 44215177.52 | 1809 | 24441.78 |
area_sale_date_mean = area_sale_date.query('area in @top_5_reg_mean.area')\
.groupby(['area', 'date_sale'])['mean_sale'].sum().reset_index()
area_sale_date_mean = (area_sale_date_mean
.pivot_table(index='date_sale',
columns='area',
values='mean_sale')
)
# устанавливаю размеры графиков
color = ['green', 'b', 'r', 'black', 'y']
area_sale_date_mean.plot(figsize=(15, 8), marker='s', color=color)
plt.xlabel('Месяц продажи')
plt.ylabel('Средняя стоимось продаж, млн. USD')
plt.title('Динамика средней стоимости продаж по топ-5 регионов по средней стоимости')
plt.show()
На графике видно, что резко выросла средняя соимость продаж в июле у Костанайской области. Поскольку в этой области развита промышленность, в том числе и автомобильная. Предполагаю, что одно из предприятий получило деньги за крупный промышленный заказ, поэтому и средняя цена выросла. Проверю.
kostanai = data.query('area == "Костанайская область"')\
.groupby(['date_sale', 'segmentation'])['sale_usd'].sum().reset_index()
dynamics(kostanai, 'date_sale', 'segmentation', 'sale_usd', 'sum', 'сегментам в Костанайской области')
Да, мое предположение подтвердилось. В июле была большая продажа в Костанайской области коммерческой техники. Посмотрю, что это были за продажи.
data.query('area == "Костанайская область" and quantity >= 10').head()
| year | month | company | brand | model | year_of_release | producing_country | type_of_fuel | engine_volume_l | transmission | type_of_drive | client_type | quantity | price_usd | sale_usd | area | segmentation | class_2013 | date_sale | engine_class | price_cat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13284 | 2019 | 2 | БИПЭК АВТО | Lada | 4x4 | 2018 | Республика Казахстан | бензин | 1.69 | механика | 4WD | юрлицо | 11 | 9000.00 | 99000.00 | Костанайская область | Внедорожники | Субкомпактные SUV | 2019-02 | малый | низкая |
| 13285 | 2019 | 2 | БИПЭК АВТО | Lada | 4x4 | 2019 | Республика Казахстан | бензин | 1.69 | механика | 4WD | юрлицо | 18 | 9000.00 | 162000.00 | Костанайская область | Внедорожники | Субкомпактные SUV | 2019-02 | малый | низкая |
| 13308 | 2019 | 3 | БИПЭК АВТО | Lada | 4x4 | 2019 | Республика Казахстан | бензин | 1.69 | механика | 4WD | юрлицо | 10 | 9000.00 | 90000.00 | Костанайская область | Внедорожники | Субкомпактные SUV | 2019-03 | малый | низкая |
| 13343 | 2019 | 4 | БИПЭК АВТО | Lada | 4x4 | 2019 | Республика Казахстан | бензин | 1.69 | механика | 4WD | юрлицо | 28 | 9000.00 | 252000.00 | Костанайская область | Внедорожники | Субкомпактные SUV | 2019-04 | малый | низкая |
| 13379 | 2019 | 5 | БИПЭК АВТО | Lada | 4x4 | 2019 | Республика Казахстан | бензин | 1.69 | механика | 4WD | юрлицо | 11 | 9000.00 | 99000.00 | Костанайская область | Внедорожники | Субкомпактные SUV | 2019-05 | малый | низкая |
Такие большие продажи совершили автомобильный хлдинг БИПЭК АВТО и Volkswagen Group Rus. Эти продажи повлияли на динамику продаж в июле.
sale_company = data.groupby('company')['sale_usd', 'quantity'].sum().reset_index()
С большим отрывом лидирует Toyota Motor Kazakhstan, на втором месте Astana Motors, на третьем - БИПЭК АВТО. Посмотрю, какую долю рынка занимают топ-10 компаний.
# диаграмма для емкости рынка в денежном выражении
fig = go.Figure(go.Pie(labels=sale_company['company'].head(10),
values=sale_company['sale_usd'],
texttemplate = "%{label} <br>%{percent: .1%}",
hole=.4,
domain=dict(x=[1, 1]),
)
)
# добавляю название
fig.update_layout(
title="Доля автоцентров на рынке Казахстана в денежном выражении",
title_x = 0.45
)
# диаграмма для количественной емкости рынка
fig = go.Figure(go.Pie(labels=sale_company['company'].head(10),
values=sale_company['quantity'],
texttemplate = "%{label} <br>%{percent: .1%}",
hole=.4,
domain=dict(x=[1, 1])
)
)
# добавляю название
fig.update_layout(
title="Доля автоцентров на рынке Казахстана в количественном выражении",
title_x = 0.45
)
fig.show()
По обоим показателям первое место занимает Astana Motors. На втором месте с большим отставанием Allur Auto. Ни в одном топе не присутствует интересующий нас автоцентр Мерку Авто.
Теперь посчитаю какие компании в каких регионах присутствуют.
company_in_reg = data.groupby(['company', 'area'])['sale_usd'].sum().unstack().rename_axis(None, axis=1)
Для каждого региона посчитаю топ-10 автоцентров.
def top_comp(df):
for column in df.columns:
display(df[column].nlargest(10).reset_index())
top_comp(company_in_reg)
| company | Акмолинская область | |
|---|---|---|
| 0 | БИПЭК АВТО | 5222572.46 |
| 1 | Astana Motors | 3070319.36 |
| 2 | Вираж | 2721749.61 |
| 3 | СемАЗ | 322199.11 |
| 4 | Subaru Kazakhstan | 231228.66 |
| 5 | Ravon Motors Kazakstan | 129034.02 |
| 6 | Allur Auto | 40785.96 |
| 7 | Renault Россия | 27022.73 |
| 8 | УзАвто-Казахстан | 20136.11 |
| 9 | Almaty Motors Premium | NaN |
| company | Актюбинская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 14812672.48 |
| 1 | ТК КАМАЗ | 7802142.98 |
| 2 | Astana Motors | 7369315.17 |
| 3 | БИПЭК АВТО | 6777640.88 |
| 4 | Nissan Manufacturing RUS | 2269416.36 |
| 5 | Вираж | 2167233.34 |
| 6 | Subaru Kazakhstan | 757996.30 |
| 7 | Renault Россия | 623715.15 |
| 8 | СемАЗ | 549106.76 |
| 9 | MMC RUS | 408091.48 |
| company | Алматинская область | |
|---|---|---|
| 0 | БИПЭК АВТО | 3286017.75 |
| 1 | Astana Motors | 2945799.35 |
| 2 | Subaru Kazakhstan | 79377.60 |
| 3 | Вираж | 73419.59 |
| 4 | Allur Auto | NaN |
| 5 | Almaty Motors Premium | NaN |
| 6 | Autokapital | NaN |
| 7 | Caspian Motors | NaN |
| 8 | Daewoo Bus Kazakhstan | NaN |
| 9 | Eurasia Motor Premium | NaN |
| company | Атырауская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 23949899.11 |
| 1 | БИПЭК АВТО | 15897406.00 |
| 2 | Astana Motors | 12284261.85 |
| 3 | Allur Auto | 5090260.02 |
| 4 | Вираж | 4594560.91 |
| 5 | Nissan Manufacturing RUS | 2883975.68 |
| 6 | Daewoo Bus Kazakhstan | 2749639.39 |
| 7 | Volkswagen Group Rus | 1522412.60 |
| 8 | Autokapital | 1086856.80 |
| 9 | Renault Россия | 953436.00 |
| company | Восточно-Казахстанская область | |
|---|---|---|
| 0 | БИПЭК АВТО | 19068068.30 |
| 1 | Toyota Motor Kazakhstan | 9425917.28 |
| 2 | Daewoo Bus Kazakhstan | 9213385.58 |
| 3 | Astana Motors | 5733479.02 |
| 4 | СемАЗ | 5065630.57 |
| 5 | Вираж | 3762044.17 |
| 6 | Автоцентр-Бавария | 3529613.68 |
| 7 | Subaru Kazakhstan | 1140781.85 |
| 8 | Renault Россия | 1130110.83 |
| 9 | Ravon Motors Kazakstan | 998181.18 |
| company | Жамбылская область | |
|---|---|---|
| 0 | БИПЭК АВТО | 5699353.00 |
| 1 | СемАЗ | 2215809.21 |
| 2 | Astana Motors | 1122531.21 |
| 3 | Toyota Motor Kazakhstan | 458151.43 |
| 4 | Вираж | 387491.66 |
| 5 | Ravon Motors Kazakstan | 198873.51 |
| 6 | Allur Auto | 12711.11 |
| 7 | Almaty Motors Premium | NaN |
| 8 | Autokapital | NaN |
| 9 | Caspian Motors | NaN |
| company | Западно-Казахстанская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 13555587.19 |
| 1 | БИПЭК АВТО | 10347066.34 |
| 2 | Astana Motors | 8908826.05 |
| 3 | Вираж | 2853351.08 |
| 4 | ТК КАМАЗ | 2622083.55 |
| 5 | Renault Россия | 1919264.26 |
| 6 | Subaru Kazakhstan | 1071360.43 |
| 7 | Автоцентр-Бавария | 1041476.23 |
| 8 | Volkswagen Group Rus | 998991.95 |
| 9 | Nissan Manufacturing RUS | 918141.68 |
| company | Карагандинская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 16875060.51 |
| 1 | БИПЭК АВТО | 10707107.59 |
| 2 | Astana Motors | 10364320.69 |
| 3 | Автомир-Центр | 4151133.25 |
| 4 | Вираж | 4136348.38 |
| 5 | Renault Россия | 3187260.88 |
| 6 | Nissan Manufacturing RUS | 2977020.78 |
| 7 | MMC RUS | 1959568.85 |
| 8 | Subaru Kazakhstan | 1519271.25 |
| 9 | Volkswagen Group Rus | 1451792.57 |
| company | Костанайская область | |
|---|---|---|
| 0 | Allur Auto | 28201849.90 |
| 1 | Toyota Motor Kazakhstan | 20522793.62 |
| 2 | Astana Motors | 11216150.80 |
| 3 | БИПЭК АВТО | 10788925.46 |
| 4 | Volkswagen Group Rus | 2998161.99 |
| 5 | Вираж | 2642381.55 |
| 6 | Nissan Manufacturing RUS | 2397493.44 |
| 7 | MMC RUS | 2017076.99 |
| 8 | Renault Россия | 1567240.40 |
| 9 | Автодом Motors KST | 1064732.01 |
| company | Кызылординская область | |
|---|---|---|
| 0 | БИПЭК АВТО | 4234788.25 |
| 1 | Astana Motors | 2995757.61 |
| 2 | Вираж | 1374207.93 |
| 3 | Ravon Motors Kazakstan | 834280.31 |
| 4 | Toyota Motor Kazakhstan | 824513.45 |
| 5 | Renault Россия | 556134.57 |
| 6 | Allur Auto | 271476.06 |
| 7 | Лифан Моторс Рус | 215286.98 |
| 8 | Subaru Kazakhstan | 118386.31 |
| 9 | СемАЗ | 69045.43 |
| company | Мангистауская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 15850073.90 |
| 1 | ТК КАМАЗ | 9351089.57 |
| 2 | БИПЭК АВТО | 8204688.25 |
| 3 | Astana Motors | 7287601.05 |
| 4 | Вираж | 2033862.70 |
| 5 | Nissan Manufacturing RUS | 1626020.21 |
| 6 | Ravon Motors Kazakstan | 1310025.04 |
| 7 | Renault Россия | 567626.38 |
| 8 | УзАвто-Казахстан | 211565.35 |
| 9 | СемАЗ | 99107.90 |
| company | Павлодарская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 12897853.50 |
| 1 | БИПЭК АВТО | 9513646.51 |
| 2 | Astana Motors | 7402731.48 |
| 3 | Вираж | 2369070.67 |
| 4 | Nissan Manufacturing RUS | 1632021.18 |
| 5 | Renault Россия | 514212.56 |
| 6 | Allur Auto | 493518.27 |
| 7 | Ravon Motors Kazakstan | 436666.87 |
| 8 | MMC RUS | 267865.06 |
| 9 | Subaru Kazakhstan | 261410.73 |
| company | Северо-Казахстанская область | |
|---|---|---|
| 0 | БИПЭК АВТО | 5947529.13 |
| 1 | Astana Motors | 2332989.19 |
| 2 | Вираж | 2298781.65 |
| 3 | Nissan Manufacturing RUS | 695002.03 |
| 4 | Subaru Kazakhstan | 523816.76 |
| 5 | ТК КАМАЗ | 473531.51 |
| 6 | Toyota Motor Kazakhstan | 425185.81 |
| 7 | Renault Россия | 354468.91 |
| 8 | Allur Auto | 259118.76 |
| 9 | СемАЗ | 40787.98 |
| company | Туркестанская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 716037.80 |
| 1 | Daewoo Bus Kazakhstan | 97134.57 |
| 2 | Ravon Motors Kazakstan | 39538.78 |
| 3 | Allur Auto | NaN |
| 4 | Almaty Motors Premium | NaN |
| 5 | Astana Motors | NaN |
| 6 | Autokapital | NaN |
| 7 | Caspian Motors | NaN |
| 8 | Eurasia Motor Premium | NaN |
| 9 | Hino Motors | NaN |
| company | Экспорт область | |
|---|---|---|
| 0 | Allur Auto | 20244728.31 |
| 1 | Toyota Motor Kazakhstan | 14326265.46 |
| 2 | Astana Motors | 3176002.29 |
| 3 | СВС-ТРАНС | 115025.91 |
| 4 | Ravon Motors Kazakstan | 20030.49 |
| 5 | Almaty Motors Premium | NaN |
| 6 | Autokapital | NaN |
| 7 | Caspian Motors | NaN |
| 8 | Daewoo Bus Kazakhstan | NaN |
| 9 | Eurasia Motor Premium | NaN |
| company | Южно-Казахстанская область | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 32531266.09 |
| 1 | Astana Motors | 23175025.31 |
| 2 | БИПЭК АВТО | 12249305.63 |
| 3 | Ravon Motors Kazakstan | 2769943.26 |
| 4 | Вираж | 2440821.17 |
| 5 | Nissan Manufacturing RUS | 2437290.09 |
| 6 | Renault Россия | 2313727.80 |
| 7 | ТК КАМАЗ | 1446442.66 |
| 8 | Subaru Kazakhstan | 1041105.08 |
| 9 | Allur Auto | 915325.36 |
| company | г.Алматы | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 176926727.19 |
| 1 | Astana Motors | 64098180.29 |
| 2 | БИПЭК АВТО | 31079212.50 |
| 3 | Nissan Manufacturing RUS | 18821630.07 |
| 4 | Mercur Auto | 15389449.83 |
| 5 | Autokapital | 14049545.64 |
| 6 | Автоцентр-Бавария | 12427881.86 |
| 7 | СВС-ТРАНС | 10198392.79 |
| 8 | Hyundai Com Trans Kazakhstan | 9710608.68 |
| 9 | Almaty Motors Premium | 9657735.08 |
| company | г.Нур-Султан | |
|---|---|---|
| 0 | Toyota Motor Kazakhstan | 125778434.62 |
| 1 | Astana Motors | 55855313.34 |
| 2 | БИПЭК АВТО | 26994441.63 |
| 3 | ТК КАМАЗ | 18729260.83 |
| 4 | Автоцентр-Бавария | 11582852.50 |
| 5 | Nissan Manufacturing RUS | 8812003.71 |
| 6 | TERRA MOTORS | 7978608.46 |
| 7 | Вираж | 4888668.23 |
| 8 | Renault Россия | 4745720.60 |
| 9 | Автомир-Центр | 4726388.78 |
Из 18 областей в топ-10 по выручке Mercur Auto попал в г.Алматы.
top_model = data.groupby('model')['sale_usd', 'quantity'].sum().reset_index()
# график топ-10 авто на рынке в долларах США
top_model = top_model.sort_values(by='sale_usd', ascending=False).head(10)
plt.figure(figsize=(15, 8))
plt.bar(top_model.model, top_model.sale_usd)
for x, y, tex in zip(top_model.model, top_model.sale_usd, top_model.sale_usd):
t = plt.text(x, y, int(tex),
horizontalalignment='center',
rotation='horizontal',
verticalalignment='bottom',
fontdict={'color':'black', 'weight':'bold'})
plt.title('Топ-10 моделей на рынке в долларах США')
plt.xticks(rotation=0)
ax = plt.gca()
ax.get_yaxis().set_visible(False)
plt.show()
На первом месте по продажам Toyota Camry с большим отрывом. Hyundai Tucson на втором месте.
# график топ-10 авто на рынке в количественном выражении
top_model = top_model.sort_values(by='quantity', ascending=False).head(10)
plt.figure(figsize=(15, 8))
plt.bar(top_model.model, top_model.quantity)
for x, y, tex in zip(top_model.model, top_model.quantity, top_model.quantity):
t = plt.text(x, y, int(tex),
horizontalalignment='center',
rotation='horizontal',
verticalalignment='bottom',
fontdict={'color':'black', 'weight':'bold'})
plt.title('Топ-10 моделей на рынке в количественном выражении')
plt.xticks(rotation=0)
ax = plt.gca()
ax.get_yaxis().set_visible(False)
plt.show()
Топ-10 практически не изменился.
Теперь посмотрю, как выглядит топ в разрезе на коммерческий и некоммерческий транспорт.
top_comm = data.query('segmentation == "Коммерческие автомобили"')\
.groupby('model')['sale_usd', 'quantity'].sum().reset_index()
top_comm.sort_values('sale_usd', ascending=False).head(10)
| model | sale_usd | quantity | |
|---|---|---|---|
| 71 | hff6850g | 19592899.74 | 240 |
| 8 | 3302 | 15664493.09 | 998 |
| 37 | 65115 | 12584301.64 | 259 |
| 17 | 43118 | 9818010.24 | 188 |
| 75 | next | 8929641.52 | 417 |
| 88 | tg | 7799873.32 | 69 |
| 46 | bc 095 | 6055573.94 | 80 |
| 87 | sx3258dr384 | 5468192.15 | 101 |
| 13 | 3909 | 4838746.38 | 390 |
| 40 | 6520 | 4683700.32 | 74 |
# сртою столбчатую диаграмму для коммерческих моделей
fig = px.bar(top_comm.sort_values('sale_usd', ascending=False).head(10),
x='sale_usd',
y='model',
color = 'model',
hover_name="model"
)
# добавляю название и подписи осей
fig.update_layout(
title="Топ-10 коммерческих моделей по продажам в USD",
xaxis_title='Продажи, USD',
yaxis_title='Модель',
title_x = 0.5
)
fig.show()
В топ-10 на первом месте ANKAI hff6850g, далее идет Камаз 3302.
top_noncomm = data.query('segmentation != "Коммерческие автомобили"')\
.groupby('model')['sale_usd', 'quantity'].sum().reset_index()
# сртою столбчатую диаграмму для некоммерческих моделей
fig = px.bar(top_noncomm.sort_values('sale_usd', ascending=False).head(10),
x='sale_usd',
y='model',
color = 'model',
hover_name="model"
)
# добавляю название и подписи осей
fig.update_layout(
title="Топ-10 некоммерческих моделей по продажам в USD",
xaxis_title='Продажи, USD',
yaxis_title='Модель',
title_x = 0.5
)
fig.show()
График некоммерческих моделей не отличается от графика по всему рынку. Первое место также занимает Toyota Camry, второе Tucson.
Общая
mercur_auto = data.query('company == "Mercur Auto"')
print(f'Общая выручка за 9 месяцев 2019 года: {mercur_auto.sale_usd.sum():_.2f} долларов'.replace('_', ' '))
Общая выручка за 9 месяцев 2019 года: 19 355 275.98 долларов
По маркам
mauto_rev_brand = mercur_auto.groupby('brand')['sale_usd', 'quantity'].sum().reset_index()
# выручка по маркам
fig = go.Figure(go.Pie(labels=mauto_rev_brand['brand'],
values=mauto_rev_brand['sale_usd'],
texttemplate = "%{label} <br>%{percent: .1%}",
hole=.4)
)
# добавляю название
fig.update_layout(
title="Выручка Mercur Auto по маркам, USD",
title_x = 0.45
)
fig.show()
Видно, что Меркур Авто сосредоточился на продаже моделей Audi, Porsche, Volkswagen. Последний автоцентру приносит основную выручку - 52,6%.
Средняя
mauto_rev_mean = (mauto_rev_brand.sale_usd.sum()) / (mauto_rev_brand.quantity.sum())
print('Средняя выручка у Меркур Авто:', round(mauto_rev_mean, 2), 'доллара')
Средняя выручка у Меркур Авто: 29777.35 доллара
Ежемесячная
mercur_rev_month = mercur_auto.groupby(['date_sale'])['sale_usd'].sum().reset_index()
# ежемесячная выручка
plt.figure(figsize=(15, 8))
plt.plot(mercur_rev_month.date_sale,
mercur_rev_month.sale_usd,
linewidth=2,
marker='s')
for x, y, tex in zip(mercur_rev_month.date_sale, mercur_rev_month.sale_usd, mercur_rev_month.sale_usd):
t = plt.text(x, y, int(tex/1000),
horizontalalignment='right',
rotation='horizontal',
verticalalignment='bottom',
fontdict={'color':'black', 'weight':'bold'})
plt.title('Ежемесечная выручка Меркур Авто (тыс.USD)')
plt.xlabel('Месяц продажи')
plt.ylabel('Объем продаж, USD')
# убираю ось Y
ax = plt.gca()
ax.get_yaxis().set_visible(False)
plt.show()
Наибольшая выручка была в мае, но после нее очень резкое падение. Буду с этим разбираться, почему так произошло.
Ежемесячная по маркам
# создам таблицу с группировкой по брендам и месяцам
mercur_rev_month_brand = mercur_auto.groupby(['date_sale', 'brand'])['sale_usd'].sum().reset_index()
dynamics(mercur_rev_month_brand, 'date_sale', 'brand', 'sale_usd', 'sum', 'моделям у Меркур Авто')
Видно, что Меркур Авто с июня перестал продавать модели Volkswagen. Возможно, что Volkswagen решил продавать свои автомобили сам. Или аналитики в Меркур Авто ошиблись в расчетах, и компания взяла на реализацию у импортера недостаточное количество автомобилей. И уже к июню все машины были распроданы.
# создаю таблицу с группировкой по областям и месяцам
mercur_rev_month_area = mercur_auto.groupby(['date_sale', 'area'])['sale_usd'].sum().reset_index()
# общие продажи по регионам
fig = go.Figure(go.Pie(labels=mercur_rev_month_area['area'],
values=mercur_rev_month_area['sale_usd'],
texttemplate = "%{label} <br>%{percent: .1%}",
hole=.4)
)
# добавляю название
fig.update_layout(
title="Доля выручки Меркур Авто по регионам, USD",
title_x = 0.45
)
fig.show()
79,5 % продаж у Меркур Авто приходится на г. Алматы. 8,5 % На г. Нур-Султан.
Теперь, посмотрю в динамике, как менялась выручка по регионам.
dynamics(mercur_rev_month_area, 'date_sale', 'area', 'sale_usd', 'sum', 'по областям у Меркур Авто')
Видно, что во всех регионах, кроме г. Алматы пропали продажи. Похоже, что в них продавали только Volkswagen, либо был спрос только на них. Это рассуждение проверю построив динамику продаж по маркам.
# функция для графиков динамики продаж
def dynamics(df):
for i, z in zip(df.area.unique(), range(len(df.area.unique()))):
color = ['green', 'b', 'r', 'black', 'y', 'm', 'c']
(df[df['area'] == i]
.pivot_table(index='date_sale',
columns='brand',
values='sale_usd',
aggfunc='sum')
.plot(figsize=(8, 4), marker='s', color=color)
)
plt.xlabel('Месяц продажи')
plt.ylabel('Объем продаж, USD')
plt.title('Динамика объема продаж автомобилей по {}'.format(i))
plt.show()
dynamics(mercur_auto)
На графиках видно, что с уходом из Меркур Авто Volkswagen и так не высокие продажи снизились еще больше.
Чтобы понять какие конкуренты, надо посмотреть в каком сегменте продает автомобили Меркур Авто.
# классы автомобилей, в которых представлен Меркур Авто
class_mercur = mercur_auto.class_2013.unique()
# регионы в которых продает Меркур Авто
area_mercur = mercur_auto.area.unique()
area_mercur
array(['г.Алматы', 'г.Нур-Султан', 'Костанайская область',
'Атырауская область', 'Карагандинская область',
'Западно-Казахстанская область'], dtype=object)
# сводная таблица по продажам в сегментах по регионам для Меркур Авто
sale_area_mercur = (mercur_auto
.pivot_table(index='class_2013',
columns='area',
values='sale_usd',
aggfunc='sum',
fill_value=0)
)
# общая сводная таблица продаж по сегментам для всех регионов
all_sale_area = pd.pivot_table(data.query('class_2013 in @class_mercur and area in @area_mercur'),
index='class_2013',
columns='area',
values='sale_usd',
aggfunc='sum',
fill_value=0)
# соединю таблицы sale_area_mercur и all_sale_area
total = sale_area_mercur.merge(all_sale_area, on='class_2013', how='left', suffixes=('', '2'))
# цикл для получения доли продаж Меркур Авто по регионам
for col in total.columns:
if not col.endswith('2'):
total[col] = total[col] / total[col + '2']
# оставляю нужные столбцы
total = total[['г.Алматы', 'г.Нур-Султан', 'Костанайская область',
'Атырауская область', 'Карагандинская область',
'Западно-Казахстанская область']]
# создаю тепловую карту, которая покажет долю рынка Меркур Авто по сегментам в регионах
plt.figure(figsize=(8, 5))
plt.title('Доля рынка по региону/сегменту у Меркур Авто')
sns.heatmap(total, linewidths=1, fmt=".1%", cbar=False, cmap='Greens', annot=True, vmin=0, center=.18);
Лучшие сегменты у Меркур Авто в г. Алматы:
Также важными для Меркур Авто являются: В и E классы в г. Алматы, полноразмерный минивэн в Костанайской области.
Посмотрю, какие есть конкуренты у Меркур Авто в этом городе по маркам автомобилей: Audi, Porsche, Volkswagen.
Конкуренты с Audi
opponents = data.query('area == "г.Алматы" and company != "Mercur Auto" and brand == "Audi"')
opponents.company.unique()
array([], dtype=object)
Нет компаний, которые бы составляли Меркур Авто конкуренцию с Audi.
Конкуренты с Porsche
opponents = data.query('area == "г.Алматы" and company != "Mercur Auto" and brand == "Porsche"')
opponents.company.unique()
array([], dtype=object)
Нет компаний, которые бы составляли Меркур Авто конкуренцию с Porsche.
Конкуренты с Volkswagen
opponents = data.query('area == "г.Алматы" and company != "Mercur Auto" and brand == "Volkswagen"')
opponents.company.unique()
array(['Volkswagen Group Rus'], dtype=object)
Единственный конкурент Меркур Авто - это Volkswagen Group Rus, производитель линейки автомобилей Volkswagen и Skoda в России. И вот этот конкурент в середине 2018 года получил статус официального поставщика представителей VW в Казахстан. И уже через год весь рынок VW взял, скорее всего, под свое крыло.
Построю матрицу BCG для анализа успешности продажи моделей у Меркур Авто. Для этого подготовлю данные.
# строю таблицу, сгруппированную по модели и дате продажи с агрегацией по сумме продажи
bcg_mercur = mercur_auto.groupby(['model', 'date_sale'])['sale_usd'].sum().reset_index()
# рассчитываю долю выручки по каждой модели от общей выручки автоцентра
bcg_mercur['perc_total'] = (bcg_mercur.groupby('model')['sale_usd'].transform('sum'))\
/ (bcg_mercur.sale_usd.sum())*100
# считаю значение темпа роста цены продажи по каждой модели
bcg_mercur['grow'] = (bcg_mercur.groupby('model')['sale_usd'].transform('last') \
/ bcg_mercur.groupby('model')['sale_usd'].transform('first'))*100
# считаю выручку по каждой модели за весь период наблюдения
bcg_mercur['total'] = bcg_mercur.groupby('model')['sale_usd'].transform('sum')
bcg_mercur.head()
| model | date_sale | sale_usd | perc_total | grow | total | |
|---|---|---|---|---|---|---|
| 0 | 911 carrera s | 2019-03 | 162576.08 | 0.84 | 100.00 | 162576.08 |
| 1 | a3 | 2019-05 | 28115.00 | 0.31 | 114.70 | 60361.99 |
| 2 | a3 | 2019-08 | 32246.99 | 0.31 | 114.70 | 60361.99 |
| 3 | a4 | 2019-04 | 32000.00 | 0.50 | 199.56 | 95858.00 |
| 4 | a4 | 2019-07 | 63858.00 | 0.50 | 199.56 | 95858.00 |
Таблицу подготовил, теперь построю саму матрицу BCG.
# устанавливаю цвета кругов в зависимости от квадранта
color_scheme = ["#0492C2"] * 11
color_scheme[5] = '#028A0F'
color_scheme[0], color_scheme[2] = '#FCAE1E', '#FCAE1E'
fig = px.scatter(bcg_mercur[bcg_mercur.perc_total > 1.2], # убираю модели, которые приносят очень мало денег
x="perc_total", y="grow",
size="total", color="model",
size_max=75,
text='model',
color_discrete_sequence=color_scheme,
title="BCG matrix Mercur Auto",
width=1000, height=600)
# добавляю квадранты матрицы
fig.add_shape(type='line', x0=0, y0=225, x1=40, y1=225, line=dict(color='black', dash='dash', width=3))
fig.add_shape(type='line', x0=20, y0=0, x1=20, y1=500, line=dict(color='black', dash='dash', width=3))
# добавляю подписи квадртантов
fig.add_annotation(x=17.5, y=210, text="<b>Хромые утки</b>", showarrow=False)
fig.add_annotation(x=17.5, y=490, text="<b>Трудные дети</b>", showarrow=False)
fig.add_annotation(x=38.5, y=490, text="<b>Звезды</b>", showarrow=False)
fig.add_annotation(x=37, y=210, text="<b>Дойные коровы</b>", showarrow=False)
# убираю легенду
fig.update_layout(showlegend=False, xaxis_title="Доля в прибыли, %", yaxis_title="Изменение объема продаж, %")
fig.show()
Звезды
У Меркур Авто нет моделей, которые занимают наибольшую долю рынка и одновременно с этим обладают выскоими темпами роста. Наиболее близки к "Звездам" Porshe Cayenne, Volkswagen Tiguan и Audi A6.
Дойные коровы
Для них характерна высокая доля прибыли при невыском темпе роста. В исследуемом автоцентре к ним относится только Volkswagen Polo.